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