diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /e2e_harness.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip | |
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'e2e_harness.zig')
| -rw-r--r-- | e2e_harness.zig | 269 |
1 files changed, 0 insertions, 269 deletions
diff --git a/e2e_harness.zig b/e2e_harness.zig deleted file mode 100644 index cd4ffd63..00000000 --- a/e2e_harness.zig +++ /dev/null @@ -1,269 +0,0 @@ -const std = @import("std"); -const posix = std.posix; -const linux = std.os.linux; -const ghostty_vt = @import("ghostty-vt"); - -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; - -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); -} - -/// E2E test harness: forks the app in a pty and parses its output with a -/// ghostty terminal emulator. Tests read the 2D screen GRID (via plainString), -/// not raw byte streams — so they're decoupled from how the app renders -/// (vaxis diff-rendering, repaints, etc). The grid is always current after -/// `pump`/`waitFor`; no setWinsize-to-force-repaint hacks needed. -/// -/// Input is sent as raw bytes to the pty (the app's vaxis parses them). The -/// harness terminal only reads output. -pub const Harness = struct { - gpa: std.mem.Allocator, - master: c_int, - pid: posix.pid_t, - /// heap-allocated so its address is stable: vtStream()'s handler holds a - /// pointer back into the Terminal, so it must not move (same reason main.zig - /// heap-allocates Term). Freed in deinit. - term: *ghostty_vt.Terminal, - stream: ghostty_vt.TerminalStream, - rows: u16, - cols: u16, - /// when true, print the captured screen state after each pump/waitFor and on - /// every assertion, so live test runs can be inspected (zig build test -Dtrace). - trace: bool = false, - /// every raw byte the app has emitted (accumulated in pump). Lets tests assert - /// on control sequences the emulator consumes and never renders (e.g. OSC 52 - /// clipboard writes). gpa-owned; freed in deinit. - raw: std.ArrayList(u8) = .empty, - - pub fn init(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16) !Harness { - return initArgs(gpa, exe, rows, cols, null); - } - - /// Like init, but passes one extra CLI arg to the app (e.g. "--tty"). - pub fn initArgs(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16, arg: ?[*:0]const u8) !Harness { - 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) { - const argv: [3:null]?[*:0]const u8 = .{ exe, arg, null }; - _ = execvp(exe, &argv); - _exit(127); - } - const term = try gpa.create(ghostty_vt.Terminal); - errdefer gpa.destroy(term); - term.* = try ghostty_vt.Terminal.init(gpa, .{ - .cols = cols, - .rows = rows, - .max_scrollback = 1024 * 1024, - }); - errdefer term.deinit(gpa); - // vtStream() captures a pointer to `term`; term is heap-allocated so the - // pointer stays valid for the life of the harness. - const stream = term.vtStream(); - return .{ - .gpa = gpa, - .master = master, - .pid = pid, - .term = term, - .stream = stream, - .rows = rows, - .cols = cols, - }; - } - - pub fn deinit(self: *Harness) void { - posix.kill(self.pid, posix.SIG.KILL) catch {}; - _ = linux.close(self.master); - self.stream.deinit(); - self.raw.deinit(self.gpa); - self.term.deinit(self.gpa); - self.gpa.destroy(self.term); - } - - /// Write raw bytes to the pty (input to the app: keystrokes, mouse events). - pub fn send(self: *Harness, 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); - } - } - - /// Read pty output and feed it to our ghostty terminal for `ms` ms. After - /// this, the grid reflects everything the app rendered so far. - pub fn pump(self: *Harness, 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.raw.appendSlice(self.gpa, buf[0..n]) catch {}; - self.stream.nextSlice(buf[0..n]); - } - } - if (self.trace) self.traceScreen("pump"); - } - - /// Resize both the pty (SIGWINCH to the app) and our terminal (grid reflows - /// to the new size). Harness terminal first so it's ready for the app's - /// re-render at the new size. - pub fn resize(self: *Harness, rows: u16, cols: u16) !void { - self.rows = rows; - self.cols = cols; - self.term.resize(self.gpa, cols, rows) catch {}; - const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; - _ = posix.system.ioctl(self.master, posix.T.IOCSWINSZ, @intFromPtr(&ws)); - } - - /// The full viewport text (plainString), newline-joined rows. Caller frees. - pub fn screenText(self: *Harness) ![]const u8 { - return self.term.plainString(self.gpa); - } - - /// Is `needle` present anywhere on the current screen? - pub fn contains(self: *Harness, needle: []const u8) bool { - const text = self.screenText() catch return false; - defer self.gpa.free(text); - return std.mem.indexOf(u8, text, needle) != null; - } - - /// Wait up to `ms` for `needle` to appear on screen, pumping bytes the - /// whole time. Replaces pollFor + setWinsize-repaint: the grid is checked - /// directly each iteration. - pub fn waitFor(self: *Harness, needle: []const u8, ms: i64) !bool { - { - const text = try self.screenText(); - defer self.gpa.free(text); - if (std.mem.indexOf(u8, text, needle) != null) { - if (self.trace) self.traceScreen("waitFor hit"); - return true; - } - } - 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.raw.appendSlice(self.gpa, buf[0..n]) catch {}; - self.stream.nextSlice(buf[0..n]); - } - const text = try self.screenText(); - defer self.gpa.free(text); - if (std.mem.indexOf(u8, text, needle) != null) { - if (self.trace) self.traceScreen("waitFor hit"); - return true; - } - } - if (self.trace) self.traceScreen("waitFor miss"); - return false; - } - - /// The visual cursor position (viewport coords): { x, y }. - pub fn cursor(self: *Harness) struct { x: u16, y: u16 } { - return .{ - .x = self.term.screens.active.cursor.x, - .y = self.term.screens.active.cursor.y, - }; - } - - /// True if the shell is at a prompt (OSC 133 semantic prompt). Requires - /// shell integration; false if the shell is outputting or has no integration. - pub fn atPrompt(self: *Harness) bool { - return self.term.cursorIsAtPrompt(); - } - - /// Print the screen grid (clean text, not raw bytes) for debugging on - /// failure. Also shows the cursor position and screen size. - pub fn dump(self: *Harness, msg: []const u8) void { - const text = self.screenText() catch { - std.debug.print("E2E FAIL: {s}\n(could not read screen)\n", .{msg}); - return; - }; - defer self.gpa.free(text); - const c = self.cursor(); - std.debug.print("E2E FAIL: {s}\nscreen {d}x{d} cursor=({d},{d}):\n{s}\n", .{ - msg, self.cols, self.rows, c.x, c.y, text, - }); - } - - /// trace-mode screen snapshot: print the current grid + cursor + size to - /// stderr (always, not just on failure). Bounded by a header so the live - /// stream of snapshots is easy to scan. - pub fn traceScreen(self: *Harness, tag: []const u8) void { - const text = self.screenText() catch return; - defer self.gpa.free(text); - const c = self.cursor(); - std.debug.print("\n[trace {s}] screen {d}x{d} cursor=({d},{d}):\n{s}\n[/trace]\n", .{ - tag, self.cols, self.rows, c.x, c.y, text, - }); - } - - // ---- assertions (dump on failure, return error.ExpectFailed) ---- - - pub fn expectWaitFor(self: *Harness, needle: []const u8, ms: i64, msg: []const u8) !void { - if (!try self.waitFor(needle, ms)) { - self.dump(msg); - return error.ExpectFailed; - } - } - - pub fn expectContains(self: *Harness, needle: []const u8, msg: []const u8) !void { - if (!self.contains(needle)) { - self.dump(msg); - return error.ExpectFailed; - } - } - - pub fn expectNotContains(self: *Harness, needle: []const u8, msg: []const u8) !void { - if (self.contains(needle)) { - self.dump(msg); - return error.ExpectFailed; - } - } - - /// Assert a byte sequence appears anywhere in the app's raw output so far. - /// For control sequences the emulator swallows and never renders (OSC 52 etc.). - pub fn expectRawContains(self: *Harness, needle: []const u8, msg: []const u8) !void { - if (std.mem.indexOf(u8, self.raw.items, needle) == null) { - std.debug.print("E2E FAIL: {s}\n(raw output has no {any})\n", .{ msg, needle }); - return error.ExpectFailed; - } - } -}; - -/// Write a file (helper for test setup, raw linux syscalls). -pub fn writeFile(path: [*:0]const u8, data: []const u8) !void { - const rc = linux.open(path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, 0o644); - const sfd: isize = @bitCast(rc); - if (sfd < 0) return error.OpenFailed; - const fd: c_int = @intCast(sfd); - defer _ = linux.close(fd); - var off: usize = 0; - while (off < data.len) { - const w = linux.write(fd, data[off..].ptr, data.len - off); - const sw: isize = @bitCast(w); - if (sw <= 0) break; - off += @intCast(sw); - } -} |
