Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 54s
235 lines
8.1 KiB
Zig
235 lines
8.1 KiB
Zig
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
|
|
|
|
/// Single-buffer test rendering pipeline for testing purposes.
|
|
pub const Renderer = struct {
|
|
allocator: Allocator,
|
|
size: Point,
|
|
screen: []Cell,
|
|
|
|
pub fn init(allocator: Allocator, size: Point) @This() {
|
|
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
|
|
@memset(screen, .{});
|
|
|
|
return .{
|
|
.allocator = allocator,
|
|
.size = size,
|
|
.screen = screen,
|
|
};
|
|
}
|
|
|
|
pub fn deinit(this: *@This()) void {
|
|
this.allocator.free(this.screen);
|
|
}
|
|
|
|
pub fn resize(this: *@This(), size: Point) !void {
|
|
this.size = size;
|
|
const n = @as(usize, size.x) * @as(usize, size.y);
|
|
|
|
this.allocator.free(this.screen);
|
|
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
|
@memset(this.screen, .{});
|
|
}
|
|
|
|
pub fn clear(this: *@This()) !void {
|
|
@memset(this.screen, .{});
|
|
}
|
|
|
|
pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void {
|
|
const size: Point = container.size;
|
|
const origin: Point = container.origin;
|
|
const cells: []const Cell = try container.content(model);
|
|
|
|
if (cells.len == 0) return;
|
|
|
|
var idx: usize = 0;
|
|
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
|
|
|
blk: for (0..size.y) |row| {
|
|
for (0..size.x) |col| {
|
|
const cell = cells[idx];
|
|
idx += 1;
|
|
|
|
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
|
|
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
|
|
|
|
if (cells.len == idx) break :blk;
|
|
}
|
|
}
|
|
// free immediately
|
|
container.allocator.free(cells);
|
|
|
|
for (container.elements.items) |*element| try this.render(T, element, Model, model);
|
|
}
|
|
|
|
pub fn save(this: @This(), writer: anytype) !void {
|
|
try std.zon.stringify.serialize(this.screen, .{ .whitespace = false }, writer);
|
|
}
|
|
};
|
|
|
|
/// This function is intended to be used only in tests. Test if a `Container`'s
|
|
/// rendered contents are equal to the expected `Cell` slice.
|
|
///
|
|
/// # Test data creation
|
|
///
|
|
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
|
|
///
|
|
/// ```zig
|
|
/// const Model = struct {};
|
|
/// var model: Model = .{};
|
|
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
|
|
/// defer file.close();
|
|
///
|
|
/// const allocator = std.testing.allocator;
|
|
/// var renderer: testing.Renderer = .init(allocator, size);
|
|
/// defer renderer.deinit();
|
|
///
|
|
/// try container.handle(&model, .{ .size = size });
|
|
/// try renderer.render(@TypeOf(container), &container, Model, &.{});
|
|
/// try renderer.save(file.writer());
|
|
/// ```
|
|
///
|
|
/// # Testing against created data
|
|
///
|
|
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
|
///
|
|
/// ```zig
|
|
/// const Model = struct {};
|
|
/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
|
/// .border = .{
|
|
/// .color = .green,
|
|
/// .sides = .all,
|
|
/// },
|
|
/// }, .{});
|
|
/// defer container.deinit();
|
|
///
|
|
/// try testing.expectContainerScreen(.{
|
|
/// .rows = 20,
|
|
/// .cols = 30,
|
|
/// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
|
|
/// ```
|
|
pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
|
|
const allocator = testing.allocator;
|
|
var renderer: Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
const model: Model = .{};
|
|
|
|
container.resize(&model, size);
|
|
container.reposition(&model, .{});
|
|
try renderer.render(T, container, Model, &model);
|
|
|
|
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
|
|
}
|
|
|
|
/// Taken from: https://codeberg.org/atman/zg/src/branch/master/src/DisplayWidth.zig
|
|
/// Owned by https://codeberg.org/atman licensed under MIT all credits for this function go to him
|
|
fn center(allocator: Allocator, str: []const u8, total_width: usize, pad: []const u8) ![]u8 {
|
|
if (str.len > total_width) return error.StrTooLong;
|
|
if (str.len == total_width) return try allocator.dupe(u8, str);
|
|
|
|
if (pad.len > total_width or str.len + pad.len > total_width) return error.PadTooLong;
|
|
|
|
const margin_width = @divFloor((total_width - str.len), 2);
|
|
if (pad.len > margin_width) return error.PadTooLong;
|
|
|
|
const extra_pad: usize = if (total_width % 2 != str.len % 2) 1 else 0;
|
|
const pads = @divFloor(margin_width, pad.len) * 2 + extra_pad;
|
|
|
|
var result = try allocator.alloc(u8, pads * pad.len + str.len);
|
|
var bytes_index: usize = 0;
|
|
var pads_index: usize = 0;
|
|
|
|
while (pads_index < pads / 2) : (pads_index += 1) {
|
|
@memcpy(result[bytes_index..][0..pad.len], pad);
|
|
bytes_index += pad.len;
|
|
}
|
|
|
|
@memcpy(result[bytes_index..][0..str.len], str);
|
|
bytes_index += str.len;
|
|
|
|
pads_index = 0;
|
|
while (pads_index < pads / 2 + extra_pad) : (pads_index += 1) {
|
|
@memcpy(result[bytes_index..][0..pad.len], pad);
|
|
bytes_index += pad.len;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// This function is intended to be used only in tests. Test if the two
|
|
/// provided cell arrays are identical. Usually the `Cell` slices are
|
|
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
|
/// `zterm.testing.expectContainerScreen` for an example usage.
|
|
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
|
|
const allocator = testing.allocator;
|
|
|
|
try testing.expectEqual(expected.len, actual.len);
|
|
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
|
|
|
|
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
|
defer expected_cps.deinit(allocator);
|
|
|
|
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
|
defer actual_cps.deinit(allocator);
|
|
|
|
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
|
|
defer allocating_writer.deinit();
|
|
|
|
var writer = &allocating_writer.writer;
|
|
var differ = false;
|
|
|
|
const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
|
|
defer allocator.free(expected_centered);
|
|
|
|
const actual_centered = try center(allocator, "Actual Screen", size.x, " ");
|
|
defer allocator.free(actual_centered);
|
|
|
|
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
|
|
|
for (origin.y..size.y) |row| {
|
|
defer {
|
|
expected_cps.clearRetainingCapacity();
|
|
actual_cps.clearRetainingCapacity();
|
|
}
|
|
for (origin.x..size.x) |col| {
|
|
const expected_cell = expected[(row * size.x) + col];
|
|
const actual_cell = actual[(row * size.x) + col];
|
|
|
|
if (!expected_cell.eql(actual_cell)) differ = true;
|
|
|
|
try expected_cps.append(allocator, expected_cell);
|
|
try actual_cps.append(allocator, actual_cell);
|
|
}
|
|
|
|
// write screens both formatted to buffer
|
|
for (expected_cps.items) |cell| try cell.value(writer);
|
|
_ = try writer.write(" ┆ ");
|
|
for (actual_cps.items) |cell| try cell.value(writer);
|
|
_ = try writer.write("\n");
|
|
}
|
|
|
|
if (!differ) return;
|
|
|
|
// test failed
|
|
var buf: [1024]u8 = undefined;
|
|
|
|
std.debug.lockStdErr();
|
|
defer std.debug.unlockStdErr();
|
|
|
|
var buffer = std.fs.File.stderr().writer(&buf);
|
|
var error_writer = &buffer.interface;
|
|
try error_writer.writeAll(writer.buffer[0..writer.end]);
|
|
try error_writer.flush();
|
|
|
|
return error.TestExpectEqualCells;
|
|
}
|
|
|
|
const std = @import("std");
|
|
const debug = std.debug;
|
|
const testing = std.testing;
|
|
const Allocator = std.mem.Allocator;
|
|
const event = @import("event.zig");
|
|
const Container = @import("container.zig").Container;
|
|
const Cell = @import("cell.zig");
|
|
const Point = @import("point.zig").Point;
|