//! 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 \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); }