diff options
| -rw-r--r-- | build.zig | 14 | ||||
| -rw-r--r-- | main.zig | 141 | ||||
| -rw-r--r-- | tests.zig | 120 |
3 files changed, 243 insertions, 32 deletions
@@ -12,6 +12,15 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("main.zig"), }), }); + const tests = b.addExecutable(.{ + .name = "pardes-e2e", + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("tests.zig"), + .link_libc = true, + }), + }); // ghostty-vt and vaxis both depend on uucode, but Zig 0.16 forbids the // same source file from belonging to two modules, so they can't each have @@ -68,4 +77,9 @@ pub fn build(b: *std.Build) void { const run = b.step("run", "descend to the pardes"); run.dependOn(&run_exe.step); + + const run_tests = b.addRunArtifact(tests); + run_tests.addArtifactArg(exe); + const test_step = b.step("test", "run deterministic tests"); + test_step.dependOn(&run_tests.step); } @@ -14,6 +14,8 @@ extern "c" fn execvp( argv: [*:null]const ?[*:0]const u8, ) c_int; +extern "c" fn _exit(status: c_int) noreturn; + // basic file navigation // basic shell io // basic layouting: for now following acme and ad @@ -32,6 +34,7 @@ pub const Command = struct { quit, tick, key_press: vaxis.Key, + pty_read: []u8, winsize: vaxis.Winsize, mouse: vaxis.Mouse, } = .nop; @@ -51,8 +54,9 @@ fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File { var master: c_int = undefined; const pid = forkpty(&master, null, null, &ws); if (pid == 0) { - _ = execvp(argv, &.{}); - std.process.exit(0); + const child_argv = [_:null]?[*:0]const u8{ argv.ptr, null }; + _ = execvp(argv.ptr, &child_argv); + _exit(127); } // not sure about the false here @@ -60,8 +64,23 @@ fn fork_pty(argv: [:0]const u8, ws: std.posix.winsize) std.Io.File { return f; } -const nrows = 16; -const ncols = 80; +fn readPty(io: std.Io, gpa: std.mem.Allocator, pty: std.Io.File, loop: *Loop) !void { + var read_buf: [0x1000]u8 = undefined; + var reader = pty.readerStreaming(io, &read_buf); + while (true) { + var buf: [0x1000]u8 = undefined; + var vec = [_][]u8{&buf}; + const n = try reader.interface.readVec(&vec); + if (n == 0) break; + const bytes = try gpa.dupe(u8, buf[0..n]); + errdefer gpa.free(bytes); + try loop.postEvent(.{ .pty_read = bytes }); + } + try loop.postEvent(.quit); +} + +const nrows = 64; +const ncols = 100; pub fn main(init: std.process.Init) !void { const io = init.io; @@ -72,10 +91,8 @@ pub fn main(init: std.process.Init) !void { .rows = nrows, }); defer term.deinit(gpa); - var tty_reader = term.vtStream(); - defer tty_reader.deinit(); - - // const str = try term.plainString(gpa); + var term_stream = term.vtStream(); + defer term_stream.deinit(); var tty_buf: [0x10000]u8 = undefined; @@ -94,50 +111,110 @@ pub fn main(init: std.process.Init) !void { var write_buf: [0x100]u8 = undefined; var writer = pty_master.writer(io, &write_buf); - std.debug.print("written {}\n", .{try writer.interface.write("ls -la /\r\n")}); + std.debug.print("written {}\n", .{try writer.interface.write("ls /\r\n")}); try writer.flush(); - var read_buf: [0x400]u8 = undefined; - var reader = pty_master.readerStreaming(io, &read_buf); - - try io.sleep(.fromMilliseconds(200), .real); - const str = try reader.interface.readAlloc(gpa, 0x400); - - tty_reader.nextSlice(str); - - defer gpa.free(str); - - const tty_str = try term.plainString(gpa); - defer gpa.free(tty_str); - - // std.debug.print("{s}", .{tty_str}); - - // try vx.setMouseMode(tty.writer(), true); + try vx.setMouseMode(tty.writer(), true); + try vx.enterAltScreen(tty.writer()); + defer vx.exitAltScreen(tty.writer()) catch {}; var loop: Loop = .init(io, &tty, &vx); try loop.start(); defer loop.stop(); + var pty_reader = try io.concurrent(readPty, .{ io, gpa, pty_master, &loop }); + defer _ = pty_reader.cancel(io) catch {}; + defer pty_master.close(io); + var dbg_print_buf: [0x1000]u8 = undefined; + while (true) { const event = try loop.nextEvent(); const win = vx.window(); - const win_child = win.child(.{ - .border = .{ .where = .all }, - .height = nrows, - .width = ncols, - }); - _ = win_child.printSegment(.{ .text = tty_str }, .{}); - switch (event) { + .mouse => |mouse| { + if (mouse.button == .wheel_down) { + term.screens.active.scroll(.{ .delta_row = 1 }); + } + + if (mouse.button == .wheel_up) { + term.screens.active.scroll(.{ .delta_row = -1 }); + } + }, .key_press => |key| { if (key.matches('c', .{ .ctrl = true })) { break; } + + if (key.matches('k', .{ .ctrl = true })) { + win.clear(); + } + + // this is not what I expected; it's just blank up or down + // if (key.matches('d', .{ .ctrl = true })) { + // try term.scrollUp(1); + // } + + if (key.matches('d', .{ .ctrl = true })) { + term.screens.active.scroll(.{ .delta_row = 15 }); + } + if (key.matches('u', .{ .ctrl = true })) { + term.screens.active.scroll(.{ .delta_row = -15 }); + } + + if (key.text) |text| { + try writer.interface.writeAll(text); + } else switch (key.codepoint) { + vaxis.Key.enter => try writer.interface.writeAll("\r"), + vaxis.Key.backspace => try writer.interface.writeAll("\x7f"), + vaxis.Key.tab => try writer.interface.writeAll("\t"), + vaxis.Key.escape => try writer.interface.writeAll("\x1b"), + vaxis.Key.up => try writer.interface.writeAll("\x1b[A"), + vaxis.Key.down => try writer.interface.writeAll("\x1b[B"), + vaxis.Key.right => try writer.interface.writeAll("\x1b[C"), + vaxis.Key.left => try writer.interface.writeAll("\x1b[D"), + else => {}, + } + try writer.flush(); + }, + .pty_read => |bytes| { + defer gpa.free(bytes); + term_stream.nextSlice(bytes); }, .winsize => |ws| try vx.resize(gpa, tty.writer(), ws), + .quit => break, else => {}, } + // term.scrollViewport(.bottom); + const tty_str = try term.plainString(gpa); + defer gpa.free(tty_str); + + const win_child = win.child(.{ + .border = .{ .where = .bottom }, + .height = nrows + 2, + .width = ncols + 2, + }); + const win_dbg = win.child(.{ + .border = .{ .where = .all }, + .x_off = ncols + 2, + .height = nrows + 2, + .width = ncols + 2, + }); + + win_child.clear(); + _ = win_child.printSegment(.{ .text = tty_str }, .{}); + win_child.showCursor(term.screens.active.cursor.x, term.screens.active.cursor.y); + + const dbg_str = try std.fmt.bufPrintSentinel( + &dbg_print_buf, + "c {} {}\npty pid {}\nwow it's working! debugging things will show here", + .{ + pty_master.handle, term.screens.active.cursor.x, term.screens.active.cursor.y, + }, + 0, + ); + _ = win_dbg.printSegment(.{ .text = dbg_str }, .{}); + try vx.render(tty.writer()); } } diff --git a/tests.zig b/tests.zig new file mode 100644 index 00000000..4cae1c26 --- /dev/null +++ b/tests.zig @@ -0,0 +1,120 @@ +const std = @import("std"); +const posix = std.posix; +const linux = std.os.linux; + +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 _exit(status: c_int) noreturn; + +fn writeAll(fd: c_int, bytes: []const u8) !void { + var off: usize = 0; + while (off < bytes.len) { + const rc = linux.write(fd, bytes[off..].ptr, bytes.len - off); + switch (posix.errno(rc)) { + .SUCCESS => off += @intCast(rc), + .INTR => {}, + else => |err| { + std.debug.print("write failed: {}\n", .{err}); + return error.WriteFailed; + }, + } + } +} + +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)); +} + +pub fn main(init: std.process.Init) !void { + const gpa = init.gpa; + const args = try init.minimal.args.toSlice(init.arena.allocator()); + if (args.len != 2) { + std.debug.print("usage: {s} <pardes-exe>\n", .{args[0]}); + return error.BadArgs; + } + + const exe = args[1]; + const marker = "PARDES_E2E"; + const command = "echo " ++ marker ++ "\r"; + + var master: c_int = undefined; + const ws = posix.winsize{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 }; + const pid = forkpty(&master, null, null, &ws); + if (pid == 0) { + const child_argv = [_:null]?[*:0]const u8{ exe.ptr, null }; + _ = execvp(exe.ptr, &child_argv); + _exit(127); + } + defer _ = linux.close(master); + defer posix.kill(pid, posix.SIG.KILL) catch {}; + + 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; + } + + 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; + } + } + + 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)], + }); + return error.MarkerNotRendered; + } +} |
