1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
//! Native-image backend harness.
//!
//! zig build image-harness fake a Kitty terminal and check
//! real transmit + placement APCs
//! zig build image-harness -Dplatform=gui run the real SDL GPU renderer,
//! capture a PPM, inspect pixels
//!
//! Unlike the text snapshot suite, both arms observe the thing the native
//! backend actually emits. The TTY arm completes vaxis's asynchronous
//! capability handshake over a PTY; the GUI arm consumes PARDES_TEST's GPU
//! readback, so a PETSCII fallback cannot accidentally satisfy either one.
const std = @import("std");
const libc = std.c;
const eh = @import("e2e_harness.zig");
pub const std_options: std.Options = .{ .log_level = .err };
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
extern "c" fn unsetenv(name: [*:0]const u8) c_int;
const gpa = std.heap.page_allocator;
const fixture_rgb = [3]u8{ 13, 77, 231 };
pub fn main(init: std.process.Init) !void {
var arena_state: std.heap.ArenaAllocator = .init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
const args = try init.minimal.args.toSlice(arena);
if (args.len != 3) return error.BadArgs;
const exe = try arena.dupeZ(u8, args[1]);
const backend = args[2];
const base = try std.fmt.allocPrintSentinel(arena, "/tmp/pardes-image-harness-{d}", .{libc.getpid()}, 0);
if (libc.mkdir(base, 0o755) != 0 and libc.errno(-1) != .EXIST) return error.MkdirFailed;
defer cleanupBase(arena, base);
_ = setenv("HOME", base, 1);
_ = setenv("HISTFILE", "/dev/null", 1);
_ = setenv("TERM", "xterm-256color", 1);
_ = setenv("LC_ALL", "C", 1);
if (std.mem.eql(u8, backend, "tty")) {
try runKitty(arena, exe, base);
} else if (std.mem.eql(u8, backend, "gui")) {
try runGui(arena, exe, base);
} else return error.BadArgs;
}
fn cleanupBase(arena: std.mem.Allocator, base: [:0]const u8) void {
const names = [_][]const u8{ "native.ppm", "latest.ppm", "latest.ppm.tmp", ".bash_history" };
for (names) |name| {
const path = std.fmt.allocPrintSentinel(arena, "{s}/{s}", .{ base, name }, 0) catch continue;
_ = libc.unlink(path);
}
_ = libc.rmdir(base);
}
fn writePpm(arena: std.mem.Allocator, path: [:0]const u8, width: usize, height: usize) !void {
var out: std.ArrayList(u8) = .empty;
try out.appendSlice(arena, try std.fmt.allocPrint(arena, "P6\n{d} {d}\n255\n", .{ width, height }));
for (0..width * height) |_| try out.appendSlice(arena, &fixture_rgb);
try eh.writeFile(path, out.items);
}
fn runKitty(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !void {
_ = unsetenv("PARDES_TEST");
_ = unsetenv("PARDES_TEST_CAPTURE_DIR");
const image_path = try std.fmt.allocPrintSentinel(arena, "{s}/native.ppm", .{base}, 0);
try writePpm(arena, image_path, 2, 2);
var h = try eh.Harness.initArgs(gpa, exe, 12, 30, image_path);
defer h.deinit();
try h.pump(150);
try h.expectRawContains("\x1b_Gi=1,a=q\x1b\\", "app did not issue the Kitty graphics capability query");
// APC says graphics is present; DA1 is the last capability response and
// closes the asynchronous handshake. Arrow-up is an ordinary queued event
// that wakes the main loop even on a terminal which supplied no in-band
// resize report, matching the documented idle-handshake edge case.
try h.send("\x1b_Gi=1;OK\x1b\\\x1b[?1;2c\x1b[A");
const deadline = eh.nowMs() + 3000;
while (eh.nowMs() < deadline) {
_ = try h.pumpOnce(50);
const sent = std.mem.indexOf(u8, h.raw.items, "\x1b_Gf=32,s=2,v=2,i=") != null;
const placed = std.mem.indexOf(u8, h.raw.items, "\x1b_Ga=p,i=") != null;
if (sent and placed) break;
}
try h.expectRawContains("\x1b_Gf=32,s=2,v=2,i=", "decoded RGBA was not transmitted to the Kitty terminal");
try h.expectRawContains("\x1b_Ga=p,i=", "transmitted Kitty image was not placed in the pane body");
// SPC t p toggles the attachment off and back on. The first transition
// must release terminal-side storage; the second must transmit again
// instead of resurrecting a stale pane-slot handle.
try h.send(" tp");
const delete_deadline = eh.nowMs() + 2000;
while (eh.nowMs() < delete_deadline and std.mem.indexOf(u8, h.raw.items, "\x1b_Ga=d,d=I,i=") == null)
_ = try h.pumpOnce(50);
try h.expectRawContains("\x1b_Ga=d,d=I,i=", "PETSCII toggle did not release the cached Kitty image");
try h.send(" tp");
const retransmit_deadline = eh.nowMs() + 2000;
while (eh.nowMs() < retransmit_deadline and std.mem.count(u8, h.raw.items, "\x1b_Gf=32,s=2,v=2,i=") < 2)
_ = try h.pumpOnce(50);
if (std.mem.count(u8, h.raw.items, "\x1b_Gf=32,s=2,v=2,i=") < 2)
return error.KittyRetransmitMissing;
std.debug.print("native image harness ok: kitty transmit + placement\n", .{});
}
fn runGui(arena: std.mem.Allocator, exe: [:0]const u8, base: [:0]const u8) !void {
const image_path = try std.fmt.allocPrintSentinel(arena, "{s}/native.ppm", .{base}, 0);
const capture_path = try std.fmt.allocPrintSentinel(arena, "{s}/latest.ppm", .{base}, 0);
try writePpm(arena, image_path, 32, 16);
_ = libc.unlink(capture_path);
_ = setenv("PARDES_TEST", "1", 1);
_ = setenv("PARDES_TEST_CAPTURE_DIR", base, 1);
_ = setenv("PARDES_TEST_COLS", "20", 1);
_ = setenv("PARDES_TEST_ROWS", "10", 1);
var h = try eh.Harness.initArgs(gpa, exe, 10, 20, image_path);
defer h.deinit();
try h.pump(1200);
const capture = readFile(gpa, capture_path) catch |err| {
std.debug.print("SDL image harness produced no capture ({t}); raw child output: {s}\n", .{ err, h.raw.items });
return err;
};
defer gpa.free(capture);
const pixels = ppmPixels(capture) orelse return error.BadCapture;
var exact: usize = 0;
var i: usize = 0;
while (i + 2 < pixels.len) : (i += 3) {
if (std.mem.eql(u8, pixels[i .. i + 3], &fixture_rgb)) exact += 1;
}
// The source contributes 512 aligned pixels. Leave ample room for driver
// filtering while still excluding glyph-art fallback and incidental UI
// colors.
if (exact < 200) {
std.debug.print("SDL image harness found only {d} native-color pixels in the GPU capture\n", .{exact});
return error.NativePixelsMissing;
}
std.debug.print("native image harness ok: SDL GPU capture ({d} source pixels)\n", .{exact});
}
fn readFile(allocator: std.mem.Allocator, path: [:0]const u8) ![]u8 {
const fd = libc.open(path, .{ .ACCMODE = .RDONLY }, @as(libc.mode_t, 0));
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
var out: std.ArrayList(u8) = .empty;
errdefer out.deinit(allocator);
var buf: [8192]u8 = undefined;
while (true) {
const n = libc.read(fd, &buf, buf.len);
if (n < 0) return error.ReadFailed;
if (n == 0) break;
try out.appendSlice(allocator, buf[0..@intCast(n)]);
}
return out.toOwnedSlice(allocator);
}
fn ppmPixels(bytes: []const u8) ?[]const u8 {
if (!std.mem.startsWith(u8, bytes, "P6\n")) return null;
var newlines: usize = 0;
for (bytes, 0..) |byte, i| if (byte == '\n') {
newlines += 1;
if (newlines == 3) return bytes[i + 1 ..];
};
return null;
}
|