summaryrefslogtreecommitdiff
path: root/test/e2e_harness.zig
diff options
context:
space:
mode:
Diffstat (limited to 'test/e2e_harness.zig')
-rw-r--r--test/e2e_harness.zig50
1 files changed, 40 insertions, 10 deletions
diff --git a/test/e2e_harness.zig b/test/e2e_harness.zig
index eb4c7a2f..c188827e 100644
--- a/test/e2e_harness.zig
+++ b/test/e2e_harness.zig
@@ -22,6 +22,24 @@ pub fn nowMs() i64 {
_ = libc.clock_gettime(.MONOTONIC, &ts);
return @as(i64, @intCast(ts.sec)) * 1000 + @divFloor(@as(i64, @intCast(ts.nsec)), 1_000_000);
}
+const raw_capture_max = 64 * 1024 * 1024;
+
+pub const RawCapture = struct {
+ bytes: [raw_capture_max]u8 = undefined,
+ items: []u8 = &.{},
+ full: bool = false,
+
+ fn appendSlice(self: *RawCapture, chunk: []const u8) error{CaptureFull}!void {
+ if (self.full) return error.CaptureFull;
+ if (chunk.len > self.bytes.len - self.items.len) {
+ self.full = true;
+ return error.CaptureFull;
+ }
+ const end = self.items.len + chunk.len;
+ @memcpy(self.bytes[self.items.len..end], chunk);
+ self.items = self.bytes[0..end];
+ }
+};
/// 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),
@@ -47,8 +65,9 @@ pub const Harness = struct {
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,
+ /// clipboard writes). Capture is bounded explicitly so a runaway child cannot
+ /// consume unbounded test-runner memory.
+ raw: *RawCapture,
pub fn init(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16) !Harness {
return initArgs(gpa, exe, rows, cols, null);
@@ -63,14 +82,16 @@ pub const Harness = struct {
/// Like init, but passes extra CLI args (e.g. "-l", "dump.zon").
pub fn initArgv(gpa: std.mem.Allocator, exe: [*:0]const u8, rows: u16, cols: u16, args: []const ?[*:0]const u8) !Harness {
+ const argv = try gpa.allocSentinel(?[*:0]const u8, args.len + 1, null);
+ defer gpa.free(argv);
+ argv[0] = exe;
+ for (args, 0..) |a, i| argv[i + 1] = a;
+
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) {
- var argv: [8:null]?[*:0]const u8 = @splat(null);
- argv[0] = exe;
- for (args, 0..) |a, i| argv[i + 1] = a;
- _ = execvp(exe, &argv);
+ _ = execvp(exe, argv.ptr);
_exit(127);
}
const term = try gpa.create(ghostty_vt.Terminal);
@@ -84,6 +105,9 @@ pub const Harness = struct {
// 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();
+ const raw = try gpa.create(RawCapture);
+ errdefer gpa.destroy(raw);
+ raw.* = .{};
return .{
.gpa = gpa,
.master = master,
@@ -92,6 +116,7 @@ pub const Harness = struct {
.stream = stream,
.rows = rows,
.cols = cols,
+ .raw = raw,
};
}
@@ -99,7 +124,7 @@ pub const Harness = struct {
posix.kill(self.pid, posix.SIG.KILL) catch {};
_ = libc.close(self.master);
self.stream.deinit();
- self.raw.deinit(self.gpa);
+ self.gpa.destroy(self.raw);
self.term.deinit(self.gpa);
self.gpa.destroy(self.term);
}
@@ -128,7 +153,7 @@ pub const Harness = struct {
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.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
}
}
@@ -147,7 +172,7 @@ pub const Harness = struct {
var buf: [4096]u8 = undefined;
const n = posix.read(self.master, &buf) catch return false;
if (n == 0) return false;
- self.raw.appendSlice(self.gpa, buf[0..n]) catch {};
+ self.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
return true;
}
@@ -206,7 +231,7 @@ pub const Harness = struct {
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.recordRaw(buf[0..n]);
self.stream.nextSlice(buf[0..n]);
}
const text = try self.screenText();
@@ -219,6 +244,11 @@ pub const Harness = struct {
if (self.trace) self.traceScreen("waitFor miss");
return false;
}
+ fn recordRaw(self: *Harness, bytes: []const u8) void {
+ self.raw.appendSlice(bytes) catch |err| switch (err) {
+ error.CaptureFull => {},
+ };
+ }
/// The visual cursor position (viewport coords): { x, y }.
pub fn cursor(self: *Harness) struct { x: u16, y: u16 } {