summaryrefslogtreecommitdiff
path: root/e2e_harness_sdl.zig
diff options
context:
space:
mode:
Diffstat (limited to 'e2e_harness_sdl.zig')
-rw-r--r--e2e_harness_sdl.zig332
1 files changed, 0 insertions, 332 deletions
diff --git a/e2e_harness_sdl.zig b/e2e_harness_sdl.zig
deleted file mode 100644
index cac25473..00000000
--- a/e2e_harness_sdl.zig
+++ /dev/null
@@ -1,332 +0,0 @@
-// E2E harness for the SDL3 GPU backend's rendered-image test mode.
-//
-// The child app runs the real SDL/GPU renderer. Input is still delivered through
-// a pty so the same terminal escape sequences exercise the same app input path,
-// but rendered frames are captured by the app as PPM image files in a temp
-// directory. Assertions inspect pixels, not the retained cell grid.
-const std = @import("std");
-const posix = std.posix;
-const linux = std.os.linux;
-
-extern "c" fn forkpty(
- amaster: *c_int,
- name: ?[*:0]u8,
- termp: ?*const anyopaque,
- winp: ?*const posix.winsize,
-) c_int;
-extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;
-extern "c" fn _exit(status: c_int) noreturn;
-extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int;
-
-fn nowMs() i64 {
- var ts: linux.timespec = undefined;
- _ = linux.clock_gettime(linux.CLOCK.MONOTONIC, &ts);
- return ts.sec * 1000 + @divFloor(@as(i64, ts.nsec), 1_000_000);
-}
-
-pub const Image = struct {
- width: u32,
- height: u32,
- pixels: []u8, // RGB, row-major
-
- pub fn deinit(self: *Image, gpa: std.mem.Allocator) void {
- gpa.free(self.pixels);
- self.* = .{ .width = 0, .height = 0, .pixels = &.{} };
- }
-
- pub fn hash(self: Image) u64 {
- return std.hash.Wyhash.hash(0, self.pixels);
- }
-
- pub fn brightPixels(self: Image, y0_in: u32, y1_in: u32, threshold: u8) usize {
- const y0 = @min(y0_in, self.height);
- const y1 = @min(y1_in, self.height);
- var n: usize = 0;
- var y = y0;
- while (y < y1) : (y += 1) {
- var x: u32 = 0;
- while (x < self.width) : (x += 1) {
- const i = (@as(usize, y) * @as(usize, self.width) + @as(usize, x)) * 3;
- const lum = (@as(u16, self.pixels[i]) + @as(u16, self.pixels[i + 1]) + @as(u16, self.pixels[i + 2])) / 3;
- if (lum >= threshold) n += 1;
- }
- }
- return n;
- }
-
- pub fn changedPixels(self: Image, other: Image, threshold: u8) usize {
- if (self.width != other.width or self.height != other.height) return std.math.maxInt(usize);
- var n: usize = 0;
- var i: usize = 0;
- while (i < self.pixels.len) : (i += 3) {
- const dr = absDiff(self.pixels[i], other.pixels[i]);
- const dg = absDiff(self.pixels[i + 1], other.pixels[i + 1]);
- const db = absDiff(self.pixels[i + 2], other.pixels[i + 2]);
- if (dr + dg + db >= threshold) n += 1;
- }
- return n;
- }
-};
-
-pub const SdlHarness = struct {
- gpa: std.mem.Allocator,
- io: std.Io,
- master: c_int,
- pid: posix.pid_t,
- rows: u16,
- cols: u16,
- trace: bool = false,
- capture_dir: [:0]u8,
- latest_path: []u8,
- image: ?Image = null,
- rbuf: std.ArrayList(u8) = .empty,
-
- pub fn init(gpa: std.mem.Allocator, io: std.Io, exe: [*:0]const u8, rows: u16, cols: u16) !SdlHarness {
- return initArgs(gpa, io, exe, rows, cols, null);
- }
-
- pub fn initArgs(gpa: std.mem.Allocator, io: std.Io, exe: [*:0]const u8, rows: u16, cols: u16, arg: ?[*:0]const u8) !SdlHarness {
- const base = ".zig-cache/pardes-sdl-e2e";
- try std.Io.Dir.cwd().createDirPath(io, base);
- const capture_dir_plain = try std.fmt.allocPrint(gpa, "{s}/{d}-{d}", .{ base, linux.getpid(), nowMs() });
- defer gpa.free(capture_dir_plain);
- const capture_dir = try gpa.dupeZ(u8, capture_dir_plain);
- errdefer gpa.free(capture_dir);
- try std.Io.Dir.cwd().createDirPath(io, capture_dir);
- const latest_path = try std.fmt.allocPrint(gpa, "{s}/latest.ppm", .{capture_dir});
- errdefer gpa.free(latest_path);
-
- var master: c_int = undefined;
- const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
- const pid = forkpty(&master, null, null, &ws);
- if (pid == 0) {
- _ = setenv("PARDES_TEST", "1", 1);
- _ = setenv("PARDES_TEST_CAPTURE_DIR", capture_dir.ptr, 1);
- var rows_buf: [16]u8 = undefined;
- var cols_buf: [16]u8 = undefined;
- const rows_z = std.fmt.bufPrintZ(&rows_buf, "{d}", .{rows}) catch unreachable;
- const cols_z = std.fmt.bufPrintZ(&cols_buf, "{d}", .{cols}) catch unreachable;
- _ = setenv("PARDES_TEST_ROWS", rows_z.ptr, 1);
- _ = setenv("PARDES_TEST_COLS", cols_z.ptr, 1);
- const argv: [3:null]?[*:0]const u8 = .{ exe, arg, null };
- _ = execvp(exe, &argv);
- _ = _exit(127);
- }
- return .{
- .gpa = gpa,
- .io = io,
- .master = master,
- .pid = pid,
- .rows = rows,
- .cols = cols,
- .capture_dir = capture_dir,
- .latest_path = latest_path,
- };
- }
-
- pub fn deinit(self: *SdlHarness) void {
- posix.kill(self.pid, posix.SIG.KILL) catch {};
- _ = linux.close(self.master);
- if (self.image) |*img| img.deinit(self.gpa);
- self.rbuf.deinit(self.gpa);
- self.gpa.free(self.latest_path);
- self.gpa.free(self.capture_dir);
- }
-
- pub fn send(self: *SdlHarness, bytes: []const u8) !void {
- var off: usize = 0;
- while (off < bytes.len) {
- const rc = linux.write(self.master, bytes[off..].ptr, bytes.len - off);
- const n: isize = @bitCast(rc);
- if (n < 0) {
- if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue;
- return error.WriteFailed;
- }
- off += @intCast(n);
- }
- }
-
- pub fn pump(self: *SdlHarness, ms: i64) !void {
- const deadline = nowMs() + ms;
- var buf: [4096]u8 = undefined;
- while (nowMs() < deadline) {
- var fds = [_]posix.pollfd{.{ .fd = self.master, .events = posix.POLL.IN, .revents = 0 }};
- _ = posix.poll(&fds, 50) catch {};
- if ((fds[0].revents & posix.POLL.IN) != 0) {
- const n = posix.read(self.master, &buf) catch break;
- if (n == 0) break;
- self.rbuf.appendSlice(self.gpa, buf[0..n]) catch {};
- }
- self.loadLatest() catch {};
- }
- self.loadLatest() catch {};
- if (self.trace) self.traceImage("pump");
- }
-
- pub fn resize(self: *SdlHarness, rows: u16, cols: u16) !void {
- self.rows = rows;
- self.cols = cols;
- const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 };
- _ = posix.system.ioctl(self.master, posix.T.IOCSWINSZ, @intFromPtr(&ws));
- }
-
- pub fn waitForImage(self: *SdlHarness, ms: i64) !bool {
- if (self.image != null) return true;
- const deadline = nowMs() + ms;
- while (nowMs() < deadline) {
- try self.pump(100);
- if (self.image != null) return true;
- }
- return false;
- }
-
- pub fn waitForChange(self: *SdlHarness, old_hash: u64, ms: i64) !bool {
- const deadline = nowMs() + ms;
- while (nowMs() < deadline) {
- try self.pump(100);
- if (self.image) |img| if (img.hash() != old_hash) return true;
- }
- return false;
- }
-
- pub fn imageHash(self: *SdlHarness) u64 {
- return if (self.image) |img| img.hash() else 0;
- }
-
- pub fn imageCopy(self: *SdlHarness) !Image {
- const img = self.image orelse return error.NoImage;
- return .{
- .width = img.width,
- .height = img.height,
- .pixels = try self.gpa.dupe(u8, img.pixels),
- };
- }
-
- pub fn cellWidth(self: *SdlHarness) u32 {
- const img = self.image orelse return 0;
- return img.width / @as(u32, self.cols);
- }
-
- pub fn cellHeight(self: *SdlHarness) u32 {
- const img = self.image orelse return 0;
- return img.height / @as(u32, self.rows);
- }
-
- pub fn expectImage(self: *SdlHarness, msg: []const u8) !void {
- if (self.image == null) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- }
-
- pub fn expectWaitForImage(self: *SdlHarness, ms: i64, msg: []const u8) !void {
- if (!try self.waitForImage(ms)) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- }
-
- pub fn expectWaitForChange(self: *SdlHarness, old_hash: u64, ms: i64, msg: []const u8) !void {
- if (!try self.waitForChange(old_hash, ms)) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- }
-
- pub fn expectGridPixels(self: *SdlHarness, msg: []const u8) !void {
- const img = self.image orelse {
- self.dump(msg);
- return error.ExpectFailed;
- };
- if (self.cols == 0 or self.rows == 0 or img.width % self.cols != 0 or img.height % self.rows != 0) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- if (img.width / self.cols < 6 or img.height / self.rows < 10) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- }
-
- pub fn expectBrightRows(self: *SdlHarness, row0: u16, row1: u16, min_bright: usize, msg: []const u8) !void {
- const img = self.image orelse {
- self.dump(msg);
- return error.ExpectFailed;
- };
- const ch = self.cellHeight();
- const y0 = @as(u32, row0) * ch;
- const y1 = @as(u32, row1) * ch;
- if (img.brightPixels(y0, y1, 60) < min_bright) {
- self.dump(msg);
- return error.ExpectFailed;
- }
- }
-
- fn loadLatest(self: *SdlHarness) !void {
- const data = std.Io.Dir.cwd().readFileAlloc(self.io, self.latest_path, self.gpa, .limited(128 * 1024 * 1024)) catch |err| switch (err) {
- error.FileNotFound => return,
- else => return err,
- };
- defer self.gpa.free(data);
- const img = try parsePpm(self.gpa, data);
- if (self.image) |*old| old.deinit(self.gpa);
- self.image = img;
- }
-
- pub fn dump(self: *SdlHarness, msg: []const u8) void {
- if (self.image) |img| {
- std.debug.print("E2E FAIL: {s}\nimage {d}x{d} hash={x} path={s}\nraw tail:\n{s}\n", .{
- msg, img.width, img.height, img.hash(), self.latest_path, rawTail(self.rbuf.items),
- });
- } else {
- std.debug.print("E2E FAIL: {s}\n(no captured image yet) path={s}\nraw tail:\n{s}\n", .{
- msg, self.latest_path, rawTail(self.rbuf.items),
- });
- }
- }
-
- pub fn traceImage(self: *SdlHarness, tag: []const u8) void {
- if (self.image) |img| {
- std.debug.print("[trace {s}] image {d}x{d} hash={x} path={s}\n", .{
- tag, img.width, img.height, img.hash(), self.latest_path,
- });
- } else {
- std.debug.print("[trace {s}] no image yet path={s}\n", .{ tag, self.latest_path });
- }
- }
-};
-
-fn parsePpm(gpa: std.mem.Allocator, data: []const u8) !Image {
- if (!std.mem.startsWith(u8, data, "P6")) return error.BadPpm;
- var idx: usize = 2;
- const w = try parsePpmInt(data, &idx);
- const h = try parsePpmInt(data, &idx);
- const max = try parsePpmInt(data, &idx);
- if (max != 255) return error.BadPpm;
- if (idx >= data.len or !isWs(data[idx])) return error.BadPpm;
- idx += 1;
- const len = @as(usize, w) * @as(usize, h) * 3;
- if (data.len < idx + len) return error.BadPpm;
- return .{ .width = w, .height = h, .pixels = try gpa.dupe(u8, data[idx .. idx + len]) };
-}
-
-fn parsePpmInt(data: []const u8, idx: *usize) !u32 {
- while (idx.* < data.len and isWs(data[idx.*])) idx.* += 1;
- const start = idx.*;
- while (idx.* < data.len and data[idx.*] >= '0' and data[idx.*] <= '9') idx.* += 1;
- if (idx.* == start) return error.BadPpm;
- return std.fmt.parseInt(u32, data[start..idx.*], 10);
-}
-
-fn isWs(b: u8) bool {
- return b == ' ' or b == '\n' or b == '\r' or b == '\t';
-}
-
-fn absDiff(a: u8, b: u8) u16 {
- return if (a >= b) @as(u16, a - b) else @as(u16, b - a);
-}
-
-fn rawTail(raw: []const u8) []const u8 {
- const max = 4096;
- return if (raw.len > max) raw[raw.len - max ..] else raw;
-}