200 lines
6.7 KiB
Zig
200 lines
6.7 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) !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 = testing.allocator;
|
|
var renderer: Renderer = .init(allocator, size);
|
|
defer renderer.deinit();
|
|
|
|
container.resize(size);
|
|
container.reposition(.{});
|
|
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 = 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();
|
|
|
|
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();
|
|
|
|
debug.lockStdErr();
|
|
defer debug.unlockStdErr();
|
|
|
|
const std_writer = std.io.getStdErr().writer();
|
|
try std_writer.writeAll(output.items);
|
|
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 DisplayWidth = @import("DisplayWidth");
|
|
const Point = @import("point.zig").Point;
|