Files
zterm/src/testing.zig
Yves Biener dddc09b4ce
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m12s
mod(container/element): remove origin: Point argument from Element.content function
Renamed `Container.contents` to `Container.content` to be in line with
the corresponding `Element` function name. This has also been done for
the properties structs used by the `Container`.
2025-03-11 07:52:35 +01:00

201 lines
6.8 KiB
Zig

//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
const std = @import("std");
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;
// TODO how would I describe the expected screens?
// - including styling?
// - compare generated strings instead? -> how would this be generated for the user?
/// Single-buffer test rendering pipeline for testing purposes.
pub const Renderer = struct {
allocator: std.mem.Allocator,
size: Point,
screen: []Cell,
pub fn init(allocator: std.mem.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) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content();
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);
}
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 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(.{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container);
/// 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
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
/// .border = .{
/// .color = .green,
/// .sides = .all,
/// },
/// }, .{});
/// defer container.deinit();
///
/// try testing.expectContainerScreen(.{
/// .rows = 20,
/// .cols = 30,
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = std.testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
try renderer.resize(size);
container.resize(.{}, size);
try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
}
/// 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 = std.testing.allocator;
try std.testing.expectEqual(expected.len, actual.len);
try std.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();
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit();
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
defer output.deinit();
var buffer = std.io.bufferedWriter(output.writer());
defer buffer.flush() catch {};
const writer = buffer.writer();
var differ = false;
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered);
const actual_centered = try dw.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(expected_cell);
try actual_cps.append(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
try buffer.flush();
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}