//! 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;