diff options
Diffstat (limited to 'tests.zig')
| -rw-r--r-- | tests.zig | 123 |
1 files changed, 64 insertions, 59 deletions
@@ -26,10 +26,45 @@ fn writeAll(fd: c_int, bytes: []const u8) !void { } } -fn waitNoHang(pid: c_int) bool { - var status: u32 = 0; - const rc = linux.waitpid(pid, &status, linux.W.NOHANG); - return posix.errno(rc) == .SUCCESS and rc == @as(usize, @intCast(pid)); +// TIOCSWINSZ on the pty master: changes the size and makes the kernel raise +// SIGWINCH in the slave's foreground process group (i.e. the app under test). +fn setWinsize(fd: c_int, rows: u16, cols: u16) !void { + const ws = posix.winsize{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 }; + const rc = linux.ioctl(fd, linux.T.IOCSWINSZ, @intFromPtr(&ws)); + if (posix.errno(rc) != .SUCCESS) return error.IoctlFailed; +} + +// Poll the pty for up to `iters` * 100ms, appending everything read into +// `output`. Returns true as soon as `needle` is present in the accumulated +// output (the app renders to the alt screen, so we just scan the byte stream). +fn pollFor( + gpa: std.mem.Allocator, + master: c_int, + output: *std.ArrayList(u8), + needle: []const u8, + iters: usize, +) !bool { + if (std.mem.indexOf(u8, output.items, needle) != null) return true; + var buf: [4096]u8 = undefined; + for (0..iters) |_| { + var fds = [_]posix.pollfd{.{ .fd = master, .events = posix.POLL.IN, .revents = 0 }}; + _ = posix.poll(&fds, 100) catch {}; + if ((fds[0].revents & (posix.POLL.IN | posix.POLL.HUP | posix.POLL.ERR)) != 0) { + const n = posix.read(master, &buf) catch break; + if (n == 0) break; + try output.appendSlice(gpa, buf[0..n]); + if (std.mem.indexOf(u8, output.items, needle) != null) return true; + } + } + return std.mem.indexOf(u8, output.items, needle) != null; +} + +fn dump(items: []const u8, msg: []const u8) void { + std.debug.print("E2E FAIL: {s}\ncaptured {d} bytes:\n{s}\n", .{ + msg, + items.len, + items[0..@min(items.len, 8192)], + }); } pub fn main(init: std.process.Init) !void { @@ -41,9 +76,8 @@ pub fn main(init: std.process.Init) !void { } const exe = args[1]; - const marker = "PARDES_E2E"; - const command = "echo " ++ marker ++ "\r"; + // Boot the app in a small pty. var master: c_int = undefined; const ws = posix.winsize{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 }; const pid = forkpty(&master, null, null, &ws); @@ -58,63 +92,34 @@ pub fn main(init: std.process.Init) !void { var output: std.ArrayList(u8) = .empty; defer output.deinit(gpa); - var buf: [4096]u8 = undefined; - for (0..20) |_| { - var fds = [_]posix.pollfd{.{ - .fd = master, - .events = posix.POLL.IN, - .revents = 0, - }}; - _ = try posix.poll(&fds, 100); - if ((fds[0].revents & posix.POLL.IN) != 0) { - const n = posix.read(master, &buf) catch break; - if (n == 0) break; - try output.appendSlice(gpa, buf[0..n]); - break; - } - } - - try writeAll(master, command); - - var saw_marker = false; - for (0..80) |_| { - var fds = [_]posix.pollfd{.{ - .fd = master, - .events = posix.POLL.IN, - .revents = 0, - }}; - _ = try posix.poll(&fds, 100); - if ((fds[0].revents & (posix.POLL.IN | posix.POLL.HUP | posix.POLL.ERR)) != 0) { - const n = posix.read(master, &buf) catch break; - if (n == 0) break; - try output.appendSlice(gpa, buf[0..n]); - if (std.mem.containsAtLeast(u8, output.items, 1, marker)) { - saw_marker = true; - break; - } - } - if (waitNoHang(pid)) break; + // 1. The app posts an initial winsize on startup. Its winsize handler bumps + // the resize counter to 1 and calls vx.resize, which forces a full + // repaint, so "resizes 1" shows up in the rendered stream. This just + // confirms the app is up and handled its first size. + if (!try pollFor(gpa, master, &output, "resizes=1", 60)) { + dump(output.items, "app never handled its initial winsize (no 'resizes=1')"); + return error.InitialResizeMissing; } - try writeAll(master, "\x03"); - for (0..20) |_| { - if (waitNoHang(pid)) break; - var fds = [_]posix.pollfd{.{ - .fd = master, - .events = posix.POLL.IN, - .revents = 0, - }}; - _ = posix.poll(&fds, 50) catch {}; - if ((fds[0].revents & posix.POLL.IN) != 0) { - _ = posix.read(master, &buf) catch 0; - } + // 2. Regression check for "resizes aren't happening": grow the outer pty. + // This delivers SIGWINCH to the app. Only if the app installed its + // resize handler will it post a new winsize event, bump the counter to + // 2, and repaint -> "resizes 2". If the handler is missing the counter + // stays at 1 forever and this times out. + try setWinsize(master, 50, 200); + if (!try pollFor(gpa, master, &output, "resizes=2", 80)) { + dump(output.items, "SIGWINCH was not turned into a resize (counter stuck at 1)"); + return error.ResizeNotDelivered; } - if (!saw_marker) { - std.debug.print("app did not render marker; captured {d} bytes:\n{s}\n", .{ - output.items.len, - output.items[0..@min(output.items.len, 4096)], - }); + // 3. Sanity: the app still forwards keystrokes to its embedded shell. + const marker = "PARDES_E2E"; + try writeAll(master, "echo " ++ marker ++ "\r"); + if (!try pollFor(gpa, master, &output, marker, 80)) { + dump(output.items, "app did not render echoed marker"); return error.MarkerNotRendered; } + + // Tell the app to quit (ctrl-c is the app's own exit key). + try writeAll(master, "\x03"); } |
