a73x

a9c14dfd

Add test-render orchestrator

a73x   2026-04-17 15:55

Iterates tests/golden/scripts/*.vt, runs waystty --capture on each,
compares via imgdiff against reference PNGs. Continues on failure.
WAYSTTY_GOLDEN_UPDATE=1 copies output to reference instead of diffing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff --git a/build.zig b/build.zig
index 56314ea..42f3089 100644
--- a/build.zig
+++ b/build.zig
@@ -353,4 +353,21 @@ pub fn build(b: *std.Build) void {
    imgdiff_test_mod.addImport("png", png_mod);
    const imgdiff_tests = b.addTest(.{ .root_module = imgdiff_test_mod });
    test_step.dependOn(&b.addRunArtifact(imgdiff_tests).step);

    const test_render_mod = b.createModule(.{
        .root_source_file = b.path("src/tools/test_render.zig"),
        .target = target,
        .optimize = optimize,
    });
    const test_render_exe = b.addExecutable(.{
        .name = "test-render",
        .root_module = test_render_mod,
    });
    b.installArtifact(test_render_exe);

    const test_render_step = b.step("test-render", "Run all golden VT scripts and diff against references");
    test_render_step.dependOn(b.getInstallStep());
    const test_render_run = b.addRunArtifact(test_render_exe);
    test_render_run.step.dependOn(b.getInstallStep());
    test_render_step.dependOn(&test_render_run.step);
}
diff --git a/src/tools/test_render.zig b/src/tools/test_render.zig
new file mode 100644
index 0000000..144e7aa
--- /dev/null
+++ b/src/tools/test_render.zig
@@ -0,0 +1,75 @@
const std = @import("std");

pub fn main() !void {
    var gpa: std.heap.DebugAllocator(.{}) = .init;
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const mode_update = blk: {
        const m = std.posix.getenv("WAYSTTY_GOLDEN_UPDATE") orelse break :blk false;
        break :blk std.mem.eql(u8, m, "1");
    };

    try std.fs.cwd().makePath("tests/golden/output");

    var scripts_dir = try std.fs.cwd().openDir("tests/golden/scripts", .{ .iterate = true });
    defer scripts_dir.close();
    var it = scripts_dir.iterate();

    var passed: usize = 0;
    var failed: usize = 0;

    while (try it.next()) |entry| {
        if (entry.kind != .file) continue;
        if (!std.mem.endsWith(u8, entry.name, ".vt")) continue;

        const base = entry.name[0 .. entry.name.len - 3];
        const script_path = try std.fmt.allocPrint(alloc, "tests/golden/scripts/{s}.vt", .{base});
        defer alloc.free(script_path);
        const output_path = try std.fmt.allocPrint(alloc, "tests/golden/output/{s}.png", .{base});
        defer alloc.free(output_path);
        const reference_path = try std.fmt.allocPrint(alloc, "tests/golden/reference/{s}.png", .{base});
        defer alloc.free(reference_path);
        const diff_path = try std.fmt.allocPrint(alloc, "tests/golden/output/{s}.diff.png", .{base});
        defer alloc.free(diff_path);

        // Run waystty --capture
        const cap = try std.process.Child.run(.{
            .allocator = alloc,
            .argv = &.{ "zig-out/bin/waystty", "--capture", script_path, output_path },
        });
        defer alloc.free(cap.stdout);
        defer alloc.free(cap.stderr);
        if (cap.term != .Exited or cap.term.Exited != 0) {
            std.debug.print("FAIL: {s}: capture exited with {}\n  stderr: {s}\n",
                .{ base, cap.term, cap.stderr });
            failed += 1;
            continue;
        }

        if (mode_update) {
            try std.fs.cwd().makePath("tests/golden/reference");
            try std.fs.cwd().copyFile(output_path, std.fs.cwd(), reference_path, .{});
            std.debug.print("UPDATED: {s}\n", .{base});
            passed += 1;
            continue;
        }

        // Run imgdiff
        const dif = try std.process.Child.run(.{
            .allocator = alloc,
            .argv = &.{ "zig-out/bin/imgdiff", output_path, reference_path, diff_path },
        });
        defer alloc.free(dif.stdout);
        defer alloc.free(dif.stderr);
        std.debug.print("{s}", .{dif.stdout});
        if (dif.term == .Exited and dif.term.Exited == 0) {
            passed += 1;
        } else {
            failed += 1;
        }
    }

    std.debug.print("\n=== test-render: {} passed, {} failed ===\n", .{ passed, failed });
    if (failed > 0) std.process.exit(1);
}