Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 55s
The renderer will provide `resize`, `reposition` and `minSize` (for the `Scrollable` `Element`) with a read-only pointer to the model of the application (similar to how it is already done for `handle` and `content`). Every interface function now has the same data that it can each use for implementing its corresponding task based on local and shared variables through the element instance and model pointer.
233 lines
8.1 KiB
Zig
233 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
|
|
debug.lockStdErr();
|
|
defer debug.unlockStdErr();
|
|
|
|
var stdout_buffer: [1024]u8 = undefined;
|
|
var stdout = std.fs.File.stdout().writer(&stdout_buffer);
|
|
const stdout_writer = &stdout.interface;
|
|
try stdout_writer.writeAll(writer.buffer[0..writer.end]);
|
|
try stdout_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;
|