summaryrefslogtreecommitdiff
path: root/smoke.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-06-17 21:40:03 -0300
committerGabriel Schneider <[email protected]>2026-06-17 23:44:24 -0300
commit15b9bef58c80e918352567e909a88084ca3134ef (patch)
treeb3d791abf5888dc94a6f4ba280119b7d92a9afd2 /smoke.zig
parentffed14b9857b53a2aae4a0c39dee47949f1331f2 (diff)
downloadpardes-vibes-ghostty.tar.gz
pardes-vibes-ghostty.zip
vibe fast vibes-ghostty
Diffstat (limited to 'smoke.zig')
-rw-r--r--smoke.zig136
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);
+}