diff options
| author | Gabriel Schneider <[email protected]> | 2026-06-17 21:40:03 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-06-17 23:44:24 -0300 |
| commit | 15b9bef58c80e918352567e909a88084ca3134ef (patch) | |
| tree | b3d791abf5888dc94a6f4ba280119b7d92a9afd2 /smoke.zig | |
| parent | ffed14b9857b53a2aae4a0c39dee47949f1331f2 (diff) | |
| download | pardes-vibes-ghostty.tar.gz pardes-vibes-ghostty.zip | |
vibe fast vibes-ghostty
Diffstat (limited to 'smoke.zig')
| -rw-r--r-- | smoke.zig | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/smoke.zig b/smoke.zig new file mode 100644 index 00000000..6b2ae164 --- /dev/null +++ b/smoke.zig @@ -0,0 +1,136 @@ +//! End-to-end smoke test: drive the real `pardes` binary under a pty, type a +//! command, resize, then exit, and verify it rendered and tore down cleanly +//! (no hang, no panic). Path to the binary comes in as argv[1] (build wires it). +//! Pure-Zig port of the former smoke.py. + +const std = @import("std"); +const posix = std.posix; + +// libc bits std.posix no longer exposes; same extern style as main.zig. +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 setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; +extern "c" fn write(fd: c_int, buf: [*]const u8, count: usize) isize; +extern "c" fn waitpid(pid: c_int, status: *c_int, options: c_int) c_int; +extern "c" fn _exit(status: c_int) noreturn; + +// ponytail: global state, single-threaded driver. The 256K capture buffer holds +// a few frames; on overflow we stop appending (search/len checks still hold). +// Grow to an ArrayList only if that limit bites. +var captured: [256 * 1024]u8 = undefined; +var cap_len: usize = 0; +var master: c_int = undefined; +var io: std.Io = undefined; + +fn send(s: []const u8) void { + _ = write(master, s.ptr, s.len); +} + +/// Pump output for `ms` milliseconds. Returns false on EOF (master closed). +fn pump(ms: i64) bool { + const end = std.Io.Timestamp.now(io, .awake).toMilliseconds() + ms; // .awake == monotonic + var buf: [65536]u8 = undefined; + while (std.Io.Timestamp.now(io, .awake).toMilliseconds() < end) { + var fds = [_]posix.pollfd{.{ .fd = master, .events = posix.POLL.IN, .revents = 0 }}; + const ready = posix.poll(&fds, 100) catch 0; + if (ready == 0 or fds[0].revents & posix.POLL.IN == 0) continue; + const n = posix.read(master, &buf) catch return false; // EIO == child gone + if (n == 0) return false; + const chunk = buf[0..n]; + if (cap_len + n <= captured.len) { + @memcpy(captured[cap_len..][0..n], chunk); + cap_len += n; + } + // Answer the queries vaxis sends, like a real terminal would. The DSR + // one (\x1b[5n -> \x1b[0n) is what wakes loop.stop()'s reader on teardown. + if (std.mem.indexOf(u8, chunk, "\x1b[5n") != null) send("\x1b[0n"); + if (std.mem.indexOf(u8, chunk, "\x1b[6n") != null) send("\x1b[1;1R"); // cursor pos -> 1,1 + if (std.mem.indexOf(u8, chunk, "\x1b[c") != null) send("\x1b[?6c"); // device attrs + if (std.mem.indexOf(u8, chunk, "\x1b[?996n") != null) send("\x1b[?997;1n"); // scheme -> dark + } + return true; +} + +pub fn main(init: std.process.Init) void { + io = init.io; + var args = init.minimal.args.iterate(); + _ = args.skip(); // argv[0] + const app: [*:0]const u8 = (args.next() orelse { + std.debug.print("usage: smoke <path-to-pardes>\n", .{}); + std.process.exit(2); + }).ptr; + + const ws = posix.winsize{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 }; + const pid = forkpty(&master, null, null, &ws); + if (pid == 0) { + _ = setenv("TERM", "xterm-256color", 1); + const argv = [_:null]?[*:0]const u8{ app, null }; + _ = execvp(app, &argv); + _exit(127); + } + + _ = pump(800); // let vaxis init + shell prompt render + send("echo PARDES_OK\r"); + _ = pump(800); + + // Exercise the resize path: shrink the pty and signal the app. + const ws_new = posix.winsize{ .row = 30, .col = 100, .xpixel = 0, .ypixel = 0 }; + _ = posix.system.ioctl(master, posix.T.IOCSWINSZ, @intFromPtr(&ws_new)); + posix.kill(pid, posix.SIG.WINCH) catch {}; + _ = pump(500); + send("echo AFTER_RESIZE\r"); + _ = pump(600); + + send("exit\r"); // shell exits -> child EOF -> app tears down + + // Wait for the app to exit, still draining output. + const deadline = std.Io.Timestamp.now(io, .awake).toMilliseconds() + 12_000; + var status: ?u32 = null; + while (std.Io.Timestamp.now(io, .awake).toMilliseconds() < deadline) { + _ = pump(300); + var st: c_int = 0; + if (waitpid(pid, &st, posix.W.NOHANG) == pid) { + status = @bitCast(st); + break; + } + } + + const out = captured[0..cap_len]; + std.debug.print("captured {d} bytes of rendered output\n", .{out.len}); + + var ok = true; + if (status) |st| { + if (posix.W.IFSIGNALED(st)) { + std.debug.print("FAIL: app killed by signal {d}\n", .{@intFromEnum(posix.W.TERMSIG(st))}); + ok = false; + } else { + const code = posix.W.EXITSTATUS(st); + std.debug.print("app exited with code {d}\n", .{code}); + if (code != 0) ok = false; + } + } else { + std.debug.print("FAIL: app did not exit within timeout (possible deadlock)\n", .{}); + posix.kill(pid, posix.SIG.KILL) catch {}; + var st: c_int = 0; + _ = waitpid(pid, &st, 0); + ok = false; + } + + for ([_][]const u8{ "panic", "segmentation", "sigsegv", "unreachable" }) |needle| { + if (std.ascii.indexOfIgnoreCase(out, needle) != null) { + std.debug.print("FAIL: '{s}' found in output\n", .{needle}); + ok = false; + } + } + + // Alt-screen enter / clear / home means vaxis painted at least one frame. + const rendered = std.mem.indexOf(u8, out, "\x1b[?1049h") != null or + std.mem.indexOf(u8, out, "\x1b[2J") != null or + std.mem.indexOf(u8, out, "\x1b[H") != null or + out.len > 200; + std.debug.print("rendered something: {}\n", .{rendered}); + if (!rendered) ok = false; + + std.debug.print("RESULT: {s}\n", .{if (ok) "PASS" else "FAIL"}); + std.process.exit(if (ok) 0 else 1); +} |
