diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-05 20:21:50 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-01 15:02:07 -0300 |
| commit | de4def3548a6729b0dfd2120495a61beef8c8c2c (patch) | |
| tree | 12144f0f64dc96bb4741459c1f9db57f44349330 /test/snapshot.zig | |
| parent | 7988d9bc6e31210ff13994f18d5622dd57ba9341 (diff) | |
| download | pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.tar.gz pardes-de4def3548a6729b0dfd2120495a61beef8c8c2c.zip | |
pardes v2: the rewrite, complete and organized. src/ (core + three shells), test/ (snapshot parity harness + 18 frozen goldens). One sans-IO core, vaxis tty + SDL3 GPU native + wasm web shells, 18/18 parity with the purged prototype, 7.6k lines vs 12.1k. Fix: gui shell pre-sized the core at init so the greet-releasing resize never fired (blank panes until first interaction); live sessions now init at defaults and get the real grid as a resize event (the shell contract, documented on Options).
Diffstat (limited to 'test/snapshot.zig')
| -rw-r--r-- | test/snapshot.zig | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/test/snapshot.zig b/test/snapshot.zig new file mode 100644 index 00000000..c1bf03c0 --- /dev/null +++ b/test/snapshot.zig @@ -0,0 +1,553 @@ +// pardes-snap: drive a pardes binary with a scripted input trace in a pty and +// capture the rendered grid at named sync points. The same script run against +// the prototype and the rewrite must produce byte-identical captures — this is +// the old-vs-new parity oracle for the rewrite. +// +// zig build snap run all snapshots/*.snap, diff goldens +// zig build snap -- --update regenerate goldens from this binary +// zig build snap -- snapshots/boot.snap one script +// +// Script commands (line-oriented, # comments): +// file <name> <content> create file in the script's cwd (before start) +// lines <name> <n> create file with n numbered lines +// dirmk <name> create a subdirectory +// start <rows> <cols> [arg] fork the app in a pty (optional extra CLI arg) +// wait <ms> <needle...> pump until needle appears on the grid (fails hard) +// settle <ms> pump for a fixed duration +// stable <quiet_ms> <timeout_ms> pump until the grid stops changing +// text <literal> send literal bytes +// send <escaped> send with \e \r \n \t \\ \xNN escapes +// key <names...> enter esc tab bs space up down left right home end +// pgup pgdn c-<ch> a-<ch> (after esc: settle >=600ms, +// or vaxis parses the next byte as alt-<ch>) +// press|release <left|middle|right> <col> <row> SGR mouse, 1-based +// drag <btn> <col> <row> motion with button held +// motion <col> <row> button-less motion (hover) +// wheel <up|down> <col> <row> +// resize <rows> <cols> +// snap <label> capture grid text + cursor +// snapstyle <label> capture per-cell style runs (fg,bg,attrs) +const std = @import("std"); +const posix = std.posix; +const linux = std.os.linux; +const ghostty_vt = @import("ghostty-vt"); +const eh = @import("e2e_harness.zig"); + +extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: c_int) c_int; +extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int; + +const SNAP_BASE = "/tmp/pardes-snap"; +const gpa = std.heap.page_allocator; + +pub fn main(init: std.process.Init) !void { + var arena_state = std.heap.ArenaAllocator.init(gpa); + defer arena_state.deinit(); + const arena = arena_state.allocator(); + const args = try init.minimal.args.toSlice(init.arena.allocator()); + + // argv: exe-under-test, then flags/scripts. All paths made absolute up + // front because runScript chdirs into a per-script workdir. + var cwd_buf: [4096]u8 = undefined; + const cwd_rc: isize = @bitCast(linux.getcwd(&cwd_buf, cwd_buf.len)); + if (cwd_rc <= 0) fatal("getcwd failed", .{}); + const orig_cwd = cwd_buf[0..@intCast(cwd_rc - 1)]; + + var exe: ?[]const u8 = null; + var update = false; + var scripts: std.ArrayList([]const u8) = .empty; + for (args[1..]) |a| { + if (std.mem.eql(u8, a, "--update")) { + update = true; + } else if (exe == null) { + exe = try absPath(arena, orig_cwd, a); + } else { + try scripts.append(arena, try absPath(arena, orig_cwd, a)); + } + } + if (exe == null) fatal("usage: pardes-snap <exe> [scripts...] [--update]", .{}); + const exe_z = try arena.dupeZ(u8, exe.?); + + if (scripts.items.len == 0) { + try listSnaps(arena, try absPath(arena, orig_cwd, "test/snapshots"), &scripts); + std.mem.sort([]const u8, scripts.items, {}, struct { + fn lt(_: void, x: []const u8, y: []const u8) bool { + return std.mem.lessThan(u8, x, y); + } + }.lt); + } + if (scripts.items.len == 0) fatal("no .snap scripts found", .{}); + + var failed: usize = 0; + for (scripts.items) |script_path| { + const stem = std.fs.path.stem(script_path); + const base_path = script_path[0 .. script_path.len - ".snap".len]; + const golden_path = try std.fmt.allocPrint(arena, "{s}.golden", .{base_path}); + const out = runScript(arena, exe_z, script_path, stem) catch |e| { + std.debug.print("FAIL {s}: script error {s}\n", .{ stem, @errorName(e) }); + failed += 1; + continue; + }; + if (update) { + try eh.writeFile(try arena.dupeZ(u8, golden_path), out); + std.debug.print("UPDATED {s} ({d} bytes)\n", .{ stem, out.len }); + } else { + const golden = readFileAlloc(arena, golden_path) catch { + std.debug.print("FAIL {s}: no golden (run with -- --update)\n", .{stem}); + failed += 1; + continue; + }; + if (std.mem.eql(u8, golden, out)) { + std.debug.print("PASS {s}\n", .{stem}); + } else { + const actual_path = try std.fmt.allocPrint(arena, "{s}.actual", .{base_path}); + try eh.writeFile(try arena.dupeZ(u8, actual_path), out); + std.debug.print("FAIL {s}: differs from golden (actual written to {s})\n", .{ stem, actual_path }); + printFirstDiff(golden, out); + failed += 1; + } + } + } + if (failed > 0) { + std.debug.print("{d}/{d} snapshot scripts FAILED\n", .{ failed, scripts.items.len }); + std.process.exit(1); + } + std.debug.print("all {d} snapshot scripts ok\n", .{scripts.items.len}); +} + +fn runScript(arena: std.mem.Allocator, exe_z: [:0]const u8, script_path: []const u8, stem: []const u8) ![]u8 { + const src = try readFileAlloc(arena, script_path); + + // Fixed, recreated-per-run workdir: its PATH shows up in pane tags, so it + // must be identical across runs and across old/new binaries. + const base = try std.fmt.allocPrint(arena, "{s}/{s}", .{ SNAP_BASE, stem }); + rmrf(base); + try mkdir(arena, SNAP_BASE, true); + try mkdir(arena, base, false); + const home = try std.fmt.allocPrint(arena, "{s}/home", .{base}); + const work = try std.fmt.allocPrint(arena, "{s}/cwd", .{base}); + try mkdir(arena, home, false); + try mkdir(arena, work, false); + // deterministic shell: fixed prompt, no history, no user rc leakage + try eh.writeFile(try std.fmt.allocPrintSentinel(arena, "{s}/.bashrc", .{home}, 0), "PS1='$ '\nHISTFILE=\n"); + _ = setenv("HOME", try arena.dupeZ(u8, home), 1); + _ = setenv("TERM", "xterm-256color", 1); + _ = setenv("LC_ALL", "C", 1); + if (chdirRc(try arena.dupeZ(u8, work)) < 0) return error.ChdirFailed; + + var out: std.ArrayList(u8) = .empty; + var h: ?eh.Harness = null; + defer if (h) |*hp| hp.deinit(); + + var lines_it = std.mem.splitScalar(u8, src, '\n'); + var lineno: usize = 0; + while (lines_it.next()) |raw_line| { + lineno += 1; + const line = std.mem.trim(u8, raw_line, " \t\r"); + if (line.len == 0 or line[0] == '#') continue; + var tok = std.mem.tokenizeScalar(u8, line, ' '); + const cmd = tok.next().?; + errdefer std.debug.print(" at {s}:{d}: {s}\n", .{ script_path, lineno, line }); + + if (std.mem.eql(u8, cmd, "file")) { + const name = tok.next() orelse return error.BadScript; + try eh.writeFile(try arena.dupeZ(u8, name), try unescape(arena, tok.rest())); + } else if (std.mem.eql(u8, cmd, "lines")) { + const name = tok.next() orelse return error.BadScript; + const n = try std.fmt.parseInt(usize, tok.next() orelse return error.BadScript, 10); + var buf: std.ArrayList(u8) = .empty; + for (1..n + 1) |i| try buf.appendSlice(arena, try std.fmt.allocPrint(arena, "line {d}\n", .{i})); + try eh.writeFile(try arena.dupeZ(u8, name), buf.items); + } else if (std.mem.eql(u8, cmd, "ppmdiag")) { + // the 64x64 diagonal-split test PPM (white/blue) from the old e2e + const name = tok.next() orelse return error.BadScript; + var ppm: std.ArrayList(u8) = .empty; + try ppm.appendSlice(arena, "P6\n64 64\n255\n"); + var py: usize = 0; + while (py < 64) : (py += 1) { + var px: usize = 0; + while (px < 64) : (px += 1) + try ppm.appendSlice(arena, if (px + py < 64) &[_]u8{ 255, 255, 255 } else &[_]u8{ 0, 0, 255 }); + } + try eh.writeFile(try arena.dupeZ(u8, name), ppm.items); + } else if (std.mem.eql(u8, cmd, "dirmk")) { + try mkdir(arena, tok.next() orelse return error.BadScript, false); + } else if (std.mem.eql(u8, cmd, "start")) { + const rows = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const cols = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + var extra: [6]?[*:0]const u8 = @splat(null); + var nextra: usize = 0; + while (tok.next()) |a| : (nextra += 1) { + if (nextra >= extra.len) return error.BadScript; + extra[nextra] = (try arena.dupeZ(u8, a)).ptr; + } + if (h) |*hp| hp.deinit(); // scripts may restart (e.g. dump then -l) + h = try eh.Harness.initArgv(gpa, exe_z.ptr, rows, cols, extra[0..nextra]); + } else if (std.mem.eql(u8, cmd, "snapfile")) { + const label = tok.next() orelse return error.BadScript; + const contents = try readFileAlloc(arena, tok.rest()); + try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== file {s}\n", .{label})); + // elide base64 payloads: they encode raw pty byte HISTORY, whose + // micro-timing (prompt redraw cycles) legitimately differs between + // implementations; the cleaned text fields are the contract. + var flines = std.mem.splitScalar(u8, contents, '\n'); + while (flines.next()) |fl| { + if (std.mem.indexOf(u8, fl, "_b64 = \"") != null) { + const eq = std.mem.indexOf(u8, fl, "=").?; + try out.appendSlice(arena, fl[0 .. eq + 1]); + try out.appendSlice(arena, " <elided>\n"); + } else { + try out.appendSlice(arena, fl); + try out.append(arena, '\n'); + } + } + try out.appendSlice(arena, "== end file\n"); + } else if (std.mem.eql(u8, cmd, "wait")) { + const ms = try std.fmt.parseInt(i64, tok.next() orelse return error.BadScript, 10); + try live(&h).expectWaitFor(tok.rest(), ms, "snapshot wait"); + } else if (std.mem.eql(u8, cmd, "settle")) { + try live(&h).pump(try std.fmt.parseInt(i64, tok.next() orelse return error.BadScript, 10)); + } else if (std.mem.eql(u8, cmd, "stable")) { + const quiet = try std.fmt.parseInt(i64, tok.next() orelse return error.BadScript, 10); + const timeout = try std.fmt.parseInt(i64, tok.next() orelse return error.BadScript, 10); + try waitStable(live(&h), quiet, timeout); + } else if (std.mem.eql(u8, cmd, "text")) { + try live(&h).send(tok.rest()); + } else if (std.mem.eql(u8, cmd, "send")) { + try live(&h).send(try unescape(arena, tok.rest())); + } else if (std.mem.eql(u8, cmd, "key")) { + while (tok.next()) |name| try live(&h).send(keyBytes(name) orelse return error.BadKey); + } else if (std.mem.eql(u8, cmd, "press") or std.mem.eql(u8, cmd, "release") or std.mem.eql(u8, cmd, "drag")) { + const btn = buttonCode(tok.next() orelse return error.BadScript) orelse return error.BadScript; + const col = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const row = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const b: u16 = if (std.mem.eql(u8, cmd, "drag")) btn + 32 else btn; + const fin: u8 = if (std.mem.eql(u8, cmd, "release")) 'm' else 'M'; + try live(&h).send(try std.fmt.allocPrint(arena, "\x1b[<{d};{d};{d}{c}", .{ b, col, row, fin })); + } else if (std.mem.eql(u8, cmd, "motion")) { + const col = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const row = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + try live(&h).send(try std.fmt.allocPrint(arena, "\x1b[<35;{d};{d}M", .{ col, row })); + } else if (std.mem.eql(u8, cmd, "wheel")) { + const dir = tok.next() orelse return error.BadScript; + const b: u16 = if (std.mem.eql(u8, dir, "up")) 64 else 65; + const col = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const row = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + try live(&h).send(try std.fmt.allocPrint(arena, "\x1b[<{d};{d};{d}M", .{ b, col, row })); + } else if (std.mem.eql(u8, cmd, "resize")) { + const rows = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + const cols = try std.fmt.parseInt(u16, tok.next() orelse return error.BadScript, 10); + try live(&h).resize(rows, cols); + } else if (std.mem.eql(u8, cmd, "snap")) { + try snapText(arena, &out, live(&h), tok.rest()); + } else if (std.mem.eql(u8, cmd, "snapstyle")) { + try snapStyle(arena, &out, live(&h), tok.rest()); + } else { + std.debug.print("unknown command {s}\n", .{cmd}); + return error.BadScript; + } + } + return out.items; +} + +fn live(h: *?eh.Harness) *eh.Harness { + if (h.*) |*v| return v; + fatal("script used the app before `start`", .{}); +} + +/// Pump until the grid is unchanged for `quiet` ms (bounded by `timeout`). +/// The one sync primitive that needs no needle — used before every snap so +/// old and new binaries are compared at rest, not mid-repaint. +fn waitStable(h: *eh.Harness, quiet: i64, timeout: i64) !void { + var total: i64 = 0; + var prev = try h.screenText(); + defer h.gpa.free(prev); + while (total < timeout) { + try h.pump(quiet); + total += quiet; + const cur = try h.screenText(); + if (std.mem.eql(u8, prev, cur)) { + h.gpa.free(cur); + return; + } + h.gpa.free(prev); + prev = cur; + } + return error.NeverStable; +} + +fn snapText(arena: std.mem.Allocator, out: *std.ArrayList(u8), h: *eh.Harness, label: []const u8) !void { + const text = try h.screenText(); + defer h.gpa.free(text); + const c = h.cursor(); + try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== snap {s} grid={d}x{d} cursor={d},{d}\n", .{ label, h.cols, h.rows, c.x, c.y })); + var it = std.mem.splitScalar(u8, text, '\n'); + var row: usize = 0; + while (row < h.rows) : (row += 1) { + const line = it.next() orelse ""; + try out.append(arena, '|'); + try out.appendSlice(arena, std.mem.trimEnd(u8, line, " ")); + try out.append(arena, '\n'); + } +} + +/// Per-row style runs from the harness emulator: `|y: x0-x1 fg,bg,attrs ...`. +/// fg/bg: d (default) | pN (palette) | #rrggbb. attrs: b f i k r v x + u U ~ . - +/// for underline single/double/curly/dotted/dashed. +fn snapStyle(arena: std.mem.Allocator, out: *std.ArrayList(u8), h: *eh.Harness, label: []const u8) !void { + try out.appendSlice(arena, try std.fmt.allocPrint(arena, "== style {s}\n", .{label})); + var y: u16 = 0; + while (y < h.rows) : (y += 1) { + try out.appendSlice(arena, try std.fmt.allocPrint(arena, "|{d}:", .{y})); + var run_buf: [64]u8 = undefined; + var key_buf: [64]u8 = undefined; + var run_key: []const u8 = ""; + var run_start: u16 = 0; + var x: u16 = 0; + while (x <= h.cols) : (x += 1) { + const key: []const u8 = if (x == h.cols) "\x00end" else cellKey(h, x, y, &key_buf); + if (x == 0) { + run_key = try arena.dupe(u8, key); + continue; + } + if (std.mem.eql(u8, key, run_key)) continue; + try out.appendSlice(arena, try std.fmt.bufPrint(&run_buf, " {d}-{d} ", .{ run_start, x - 1 })); + try out.appendSlice(arena, run_key); + run_key = try arena.dupe(u8, key); + run_start = x; + } + try out.append(arena, '\n'); + } +} + +fn cellKey(h: *eh.Harness, x: u16, y: u16, buf: *[64]u8) []const u8 { + const ci = h.term.screens.active.pages.getCell(.{ .viewport = .{ .x = x, .y = y } }) orelse return "d,d,"; + if (ci.cell.wide == .spacer_tail) return "d,d,"; + const style = ci.style(); + var n: usize = 0; + var tmp: [16]u8 = undefined; + const fg = colorStr(style.fg_color, &tmp); + @memcpy(buf[n..][0..fg.len], fg); + n += fg.len; + buf[n] = ','; + n += 1; + var tmp2: [16]u8 = undefined; + const bg: []const u8 = switch (ci.cell.content_tag) { + .bg_color_palette => std.fmt.bufPrint(&tmp2, "p{d}", .{ci.cell.content.color_palette.data}) catch "?", + .bg_color_rgb => blk: { + const rgb = ci.cell.content.color_rgb; + break :blk std.fmt.bufPrint(&tmp2, "#{x:0>2}{x:0>2}{x:0>2}", .{ rgb.r, rgb.g, rgb.b }) catch "?"; + }, + else => colorStr(style.bg_color, &tmp2), + }; + @memcpy(buf[n..][0..bg.len], bg); + n += bg.len; + buf[n] = ','; + n += 1; + if (style.flags.bold) { + buf[n] = 'b'; + n += 1; + } + if (style.flags.faint) { + buf[n] = 'f'; + n += 1; + } + if (style.flags.italic) { + buf[n] = 'i'; + n += 1; + } + if (style.flags.blink) { + buf[n] = 'k'; + n += 1; + } + if (style.flags.inverse) { + buf[n] = 'r'; + n += 1; + } + if (style.flags.invisible) { + buf[n] = 'v'; + n += 1; + } + if (style.flags.strikethrough) { + buf[n] = 'x'; + n += 1; + } + const ul: []const u8 = switch (style.flags.underline) { + .none => "", + .single => "u", + .double => "U", + .curly => "~", + .dotted => ".", + .dashed => "-", + }; + @memcpy(buf[n..][0..ul.len], ul); + n += ul.len; + return buf[0..n]; +} + +fn colorStr(color: ghostty_vt.Style.Color, buf: *[16]u8) []const u8 { + return switch (color) { + .none => "d", + .palette => |idx| std.fmt.bufPrint(buf, "p{d}", .{idx}) catch "?", + .rgb => |rgb| std.fmt.bufPrint(buf, "#{x:0>2}{x:0>2}{x:0>2}", .{ rgb.r, rgb.g, rgb.b }) catch "?", + }; +} + +fn keyBytes(name: []const u8) ?[]const u8 { + const map = .{ + .{ "enter", "\r" }, .{ "esc", "\x1b" }, .{ "tab", "\t" }, + .{ "bs", "\x7f" }, .{ "space", " " }, .{ "up", "\x1b[A" }, + .{ "down", "\x1b[B" }, .{ "right", "\x1b[C" }, .{ "left", "\x1b[D" }, + .{ "home", "\x1b[H" }, .{ "end", "\x1b[F" }, .{ "pgup", "\x1b[5~" }, + .{ "pgdn", "\x1b[6~" }, + }; + inline for (map) |e| if (std.mem.eql(u8, name, e[0])) return e[1]; + const S = struct { + var buf: [2]u8 = undefined; + }; + if (name.len == 1) { + S.buf[0] = name[0]; + return S.buf[0..1]; + } + if (name.len == 3 and name[1] == '-') { + if (name[0] == 'c' and name[2] >= 'a' and name[2] <= 'z') { + S.buf[0] = name[2] - 'a' + 1; + return S.buf[0..1]; + } + if (name[0] == 'a') { + S.buf = .{ 0x1b, name[2] }; + return S.buf[0..2]; + } + } + return null; +} + +fn buttonCode(name: []const u8) ?u16 { + if (std.mem.eql(u8, name, "left")) return 0; + if (std.mem.eql(u8, name, "middle")) return 1; + if (std.mem.eql(u8, name, "right")) return 2; + return null; +} + +fn unescape(arena: std.mem.Allocator, s: []const u8) ![]const u8 { + var buf: std.ArrayList(u8) = .empty; + var i: usize = 0; + while (i < s.len) : (i += 1) { + if (s[i] != '\\' or i + 1 >= s.len) { + try buf.append(arena, s[i]); + continue; + } + i += 1; + switch (s[i]) { + 'e' => try buf.append(arena, 0x1b), + 'r' => try buf.append(arena, '\r'), + 'n' => try buf.append(arena, '\n'), + 't' => try buf.append(arena, '\t'), + '\\' => try buf.append(arena, '\\'), + 'x' => { + if (i + 2 >= s.len) return error.BadEscape; + try buf.append(arena, try std.fmt.parseInt(u8, s[i + 1 .. i + 3], 16)); + i += 2; + }, + else => return error.BadEscape, + } + } + return buf.items; +} + +fn absPath(arena: std.mem.Allocator, cwd: []const u8, p: []const u8) ![]const u8 { + if (p.len > 0 and p[0] == '/') return try arena.dupe(u8, p); + return try std.fmt.allocPrint(arena, "{s}/{s}", .{ cwd, p }); +} + +fn listSnaps(arena: std.mem.Allocator, dir_path: []const u8, scripts: *std.ArrayList([]const u8)) !void { + const dir_z = try arena.dupeZ(u8, dir_path); + const rc = linux.open(dir_z, .{ .ACCMODE = .RDONLY, .DIRECTORY = true }, 0); + const sfd: isize = @bitCast(rc); + if (sfd < 0) fatal("no snapshots/ dir at {s}", .{dir_path}); + const fd: i32 = @intCast(sfd); + defer _ = linux.close(fd); + var buf: [8192]u8 align(@alignOf(linux.dirent64)) = undefined; + while (true) { + const r = linux.getdents64(fd, &buf, buf.len); + const n: isize = @bitCast(r); + if (n <= 0) break; + var off: usize = 0; + while (off < @as(usize, @intCast(n))) { + const d: *align(1) linux.dirent64 = @ptrCast(&buf[off]); + const name = std.mem.span(@as([*:0]const u8, @ptrCast(&d.name))); + if (std.mem.endsWith(u8, name, ".snap")) + try scripts.append(arena, try std.fmt.allocPrint(arena, "{s}/{s}", .{ dir_path, name })); + off += d.reclen; + } + } +} + +fn readFileAlloc(arena: std.mem.Allocator, path: []const u8) ![]u8 { + const path_z = try arena.dupeZ(u8, path); + const rc = linux.open(path_z, .{ .ACCMODE = .RDONLY }, 0); + const sfd: isize = @bitCast(rc); + if (sfd < 0) return error.OpenFailed; + const fd: i32 = @intCast(sfd); + defer _ = linux.close(fd); + var buf: std.ArrayList(u8) = .empty; + var chunk: [16384]u8 = undefined; + while (true) { + const r = linux.read(fd, &chunk, chunk.len); + const n: isize = @bitCast(r); + if (n < 0) { + if (n == -@as(isize, @intFromEnum(linux.E.INTR))) continue; + return error.ReadFailed; + } + if (n == 0) break; + try buf.appendSlice(arena, chunk[0..@intCast(n)]); + } + return buf.items; +} + +fn mkdir(arena: std.mem.Allocator, path: []const u8, exist_ok: bool) !void { + const path_z = try arena.dupeZ(u8, path); + const n: isize = @bitCast(linux.mkdir(path_z, 0o755)); + if (n < 0) { + if (exist_ok and n == -@as(isize, @intFromEnum(linux.E.EXIST))) return; + return error.MkdirFailed; + } +} + +fn chdirRc(path_z: [:0]const u8) isize { + return @bitCast(linux.chdir(path_z.ptr)); +} + +fn rmrf(path: []const u8) void { + var path_buf: [4096:0]u8 = undefined; + if (path.len >= path_buf.len) return; + @memcpy(path_buf[0..path.len], path); + path_buf[path.len] = 0; + const pid: isize = @bitCast(linux.fork()); + if (pid == 0) { + const argv: [4:null]?[*:0]const u8 = .{ "rm", "-rf", @as([*:0]const u8, @ptrCast(&path_buf)), null }; + _ = execvp("rm", &argv); + linux.exit_group(127); + } + if (pid < 0) return; + var status: u32 = 0; + _ = linux.wait4(@intCast(pid), &status, 0, null); +} + +fn printFirstDiff(golden: []const u8, actual: []const u8) void { + var gi = std.mem.splitScalar(u8, golden, '\n'); + var ai = std.mem.splitScalar(u8, actual, '\n'); + var n: usize = 1; + while (true) : (n += 1) { + const g = gi.next(); + const a = ai.next(); + if (g == null and a == null) return; + if (g != null and a != null and std.mem.eql(u8, g.?, a.?)) continue; + std.debug.print(" first diff at golden line {d}:\n -{s}\n +{s}\n", .{ n, g orelse "<eof>", a orelse "<eof>" }); + return; + } +} + +fn fatal(comptime fmt: []const u8, args: anytype) noreturn { + std.debug.print(fmt ++ "\n", args); + std.process.exit(1); +} |
