diff options
Diffstat (limited to 'e2e_harness_sdl.zig')
| -rw-r--r-- | e2e_harness_sdl.zig | 332 |
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; -} |
