summaryrefslogtreecommitdiff
path: root/test/hxdiff.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-09 10:41:33 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a (patch)
tree91fce278abc85e4f7491de8b5f02184c8a2d5a56 /test/hxdiff.zig
parent2b1fac6fecbf7d7cc08f9dad24687b05e9968c9a (diff)
downloadpardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.tar.gz
pardes-9085cb5bfdd0b78ff3a62c0c71fc231dd7b5052a.zip
replace ArrayLists with bounded storage
Diffstat (limited to 'test/hxdiff.zig')
-rw-r--r--test/hxdiff.zig163
1 files changed, 111 insertions, 52 deletions
diff --git a/test/hxdiff.zig b/test/hxdiff.zig
index f6ab5c6f..599390d6 100644
--- a/test/hxdiff.zig
+++ b/test/hxdiff.zig
@@ -90,7 +90,8 @@ pub fn main(init: std.process.Init) !void {
var parity = false;
var goldens_path: ?[]const u8 = null;
var waivers_path: ?[]const u8 = null;
- var case_paths: std.ArrayList([]const u8) = .empty;
+ const case_paths = arena.alloc([]const u8, args.len - 1) catch fatal("oom", .{});
+ var n_case_paths: usize = 0;
var want_waivers = false;
for (args[1..]) |a| {
if (want_waivers) {
@@ -100,17 +101,18 @@ pub fn main(init: std.process.Init) !void {
parity = true;
} else if (std.mem.eql(u8, a, "--waivers")) {
want_waivers = true;
- } else if (parity or case_paths.items.len == 0) {
+ } else if (parity or n_case_paths == 0) {
// diff mode is positional (cases, goldens, waivers); parity mode
// takes any number of case files and nothing else
- case_paths.append(arena, a) catch fatal("oom", .{});
+ case_paths[n_case_paths] = a;
+ n_case_paths += 1;
} else if (goldens_path == null) {
goldens_path = a;
} else if (waivers_path == null) {
waivers_path = a;
} else fatal("usage: pardes-hxdiff <cases.jsonl> [goldens.jsonl [waivers.jsonl]]", .{});
}
- if (case_paths.items.len == 0)
+ if (n_case_paths == 0)
fatal("usage: pardes-hxdiff [--parity] <cases.jsonl>... [goldens.jsonl [waivers.jsonl]]", .{});
// goldens + waivers, keyed by name (diff mode)
@@ -144,11 +146,11 @@ pub fn main(init: std.process.Init) !void {
}
}
- var out: std.ArrayList(u8) = .empty;
+ const stdout = std.Io.File.stdout();
var n_cases: usize = 0;
var n_bad: usize = 0;
var n_waived: usize = 0;
- for (case_paths.items) |path| {
+ for (case_paths[0..n_case_paths]) |path| {
const src = readFileAlloc(arena, path) catch |e| fatal("read {s}: {s}", .{ path, @errorName(e) });
var lines = std.mem.splitScalar(u8, src, '\n');
var lineno: usize = 0;
@@ -179,8 +181,8 @@ pub fn main(init: std.process.Init) !void {
// null sels/primary are DROPPED, not written: that is what
// keeps a one-selection line identical to the old contract
const jl = std.json.Stringify.valueAlloc(arena, res, .{ .emit_null_optional_fields = false }) catch fatal("oom", .{});
- out.appendSlice(arena, jl) catch fatal("oom", .{});
- out.append(arena, '\n') catch fatal("oom", .{});
+ try stdout.writeStreamingAll(init.io, jl);
+ try stdout.writeStreamingAll(init.io, "\n");
continue;
}
const g = goldens.get(case.name) orelse fatal("no golden for case {s} (regen goldens?)", .{case.name});
@@ -195,10 +197,7 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("MISMATCH {s} (keys: {s})\n{s}", .{ case.name, case.keys, diff });
}
}
- if (!parity and goldens_path == null) {
- try std.Io.File.stdout().writeStreamingAll(init.io, out.items);
- return;
- }
+ if (!parity and goldens_path == null) return;
std.debug.print("{s}: {d} cases, {d} mismatches, {d} waived\n", .{ if (parity) "hxparity" else "hxdiff", n_cases, n_bad, n_waived });
if (n_bad > 0) std.process.exit(1);
}
@@ -235,18 +234,23 @@ const Waiver = struct {
/// field-by-field compare; returns a human-readable report ("" = match).
/// `wl`/`gl` name the two sides (helix vs pardes, or file vs pty).
fn diffResult(arena: std.mem.Allocator, want: Result, got: Result, wl: []const u8, gl: []const u8) []const u8 {
- var buf: std.ArrayList(u8) = .empty;
+ var parts: [5][]const u8 = undefined;
+ var n: usize = 0;
if (!std.mem.eql(u8, want.text, got.text)) {
- appendf(arena, &buf, " text: {s} {f} != {s} {f}\n", .{ wl, std.json.fmt(want.text, .{}), gl, std.json.fmt(got.text, .{}) });
+ parts[n] = std.fmt.allocPrint(arena, " text: {s} {f} != {s} {f}\n", .{ wl, std.json.fmt(want.text, .{}), gl, std.json.fmt(got.text, .{}) }) catch "";
+ n += 1;
}
if (!std.mem.eql(u8, want.mode, got.mode)) {
- appendf(arena, &buf, " mode: {s} {s} != {s} {s}\n", .{ wl, want.mode, gl, got.mode });
+ parts[n] = std.fmt.allocPrint(arena, " mode: {s} {s} != {s} {s}\n", .{ wl, want.mode, gl, got.mode }) catch "";
+ n += 1;
}
if (want.cursor.row != got.cursor.row or want.cursor.col != got.cursor.col) {
- appendf(arena, &buf, " cursor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.cursor.row), @as(i64, want.cursor.col), gl, @as(i64, got.cursor.row), @as(i64, got.cursor.col) });
+ parts[n] = std.fmt.allocPrint(arena, " cursor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.cursor.row), @as(i64, want.cursor.col), gl, @as(i64, got.cursor.row), @as(i64, got.cursor.col) }) catch "";
+ n += 1;
}
if (want.anchor.row != got.anchor.row or want.anchor.col != got.anchor.col) {
- appendf(arena, &buf, " anchor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.anchor.row), @as(i64, want.anchor.col), gl, @as(i64, got.anchor.row), @as(i64, got.anchor.col) });
+ parts[n] = std.fmt.allocPrint(arena, " anchor: {s} ({d},{d}) != {s} ({d},{d})\n", .{ wl, @as(i64, want.anchor.row), @as(i64, want.anchor.col), gl, @as(i64, got.anchor.row), @as(i64, got.anchor.col) }) catch "";
+ n += 1;
}
// the whole selection: absent on both sides = one range, and the two
// fields above already said everything about it
@@ -258,31 +262,58 @@ fn diffResult(arena: std.mem.Allocator, want: Result, got: Result, wl: []const u
a.anchor.row != b.anchor.row or a.anchor.col != b.anchor.col) sel_diff = true;
};
if (sel_diff) {
- appendf(arena, &buf, " sels: {s} {s} != {s} {s}\n", .{ wl, fmtSels(arena, ws, want.primary), gl, fmtSels(arena, gs, got.primary) });
+ parts[n] = std.fmt.allocPrint(arena, " sels: {s} {s} != {s} {s}\n", .{ wl, fmtSels(arena, ws, want.primary), gl, fmtSels(arena, gs, got.primary) }) catch "";
+ n += 1;
}
- return buf.items;
+ return concatParts(arena, parts[0..n]);
}
/// "2 of [(0,1)|(1,1) *(2,1)]" — every range as cursor|anchor (collapsed to
/// one pair when they are the same cell), the primary starred
fn fmtSels(arena: std.mem.Allocator, sels: []const Sel, primary: ?usize) []const u8 {
if (sels.len == 0) return "<one>";
- var buf: std.ArrayList(u8) = .empty;
- appendf(arena, &buf, "{d} of [", .{sels.len});
+ var part_count: usize = 2;
for (sels, 0..) |s, i| {
- if (i > 0) appendf(arena, &buf, " ", .{});
- if (i == (primary orelse 0)) appendf(arena, &buf, "*", .{});
- appendf(arena, &buf, "({d},{d})", .{ @as(i64, s.cursor.row), @as(i64, s.cursor.col) });
- if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col)
- appendf(arena, &buf, "|({d},{d})", .{ @as(i64, s.anchor.row), @as(i64, s.anchor.col) });
+ part_count += 1;
+ if (i > 0) part_count += 1;
+ if (i == (primary orelse 0)) part_count += 1;
+ if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col) part_count += 1;
}
- appendf(arena, &buf, "]", .{});
- return buf.items;
+ const parts = arena.alloc([]const u8, part_count) catch return "";
+ var n: usize = 0;
+ parts[n] = std.fmt.allocPrint(arena, "{d} of [", .{sels.len}) catch "";
+ n += 1;
+ for (sels, 0..) |s, i| {
+ if (i > 0) {
+ parts[n] = " ";
+ n += 1;
+ }
+ if (i == (primary orelse 0)) {
+ parts[n] = "*";
+ n += 1;
+ }
+ parts[n] = std.fmt.allocPrint(arena, "({d},{d})", .{ @as(i64, s.cursor.row), @as(i64, s.cursor.col) }) catch "";
+ n += 1;
+ if (s.anchor.row != s.cursor.row or s.anchor.col != s.cursor.col) {
+ parts[n] = std.fmt.allocPrint(arena, "|({d},{d})", .{ @as(i64, s.anchor.row), @as(i64, s.anchor.col) }) catch "";
+ n += 1;
+ }
+ }
+ parts[n] = "]";
+ n += 1;
+ return concatParts(arena, parts[0..n]);
}
-fn appendf(arena: std.mem.Allocator, buf: *std.ArrayList(u8), comptime fmt: []const u8, fargs: anytype) void {
- const s = std.fmt.allocPrint(arena, fmt, fargs) catch return;
- buf.appendSlice(arena, s) catch {};
+fn concatParts(arena: std.mem.Allocator, parts: []const []const u8) []const u8 {
+ var len: usize = 0;
+ for (parts) |part| len = std.math.add(usize, len, part.len) catch return "";
+ const out = arena.alloc(u8, len) catch return "";
+ var offset: usize = 0;
+ for (parts) |part| {
+ @memcpy(out[offset..][0..part.len], part);
+ offset += part.len;
+ }
+ return out;
}
fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result {
@@ -297,12 +328,18 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
_ = try core.hxOpenFileContent(case.text);
} else if (std.mem.eql(u8, pane_kind, "tty")) {
// the harness is the pty here: cook \n to \r\n the way ONLCR would
- var cooked: std.ArrayList(u8) = .empty;
+ const cooked_len = std.math.add(usize, case.text.len, std.mem.count(u8, case.text, "\n")) catch return error.OutOfMemory;
+ const cooked = try arena.alloc(u8, cooked_len);
+ var cooked_i: usize = 0;
for (case.text) |ch| {
- if (ch == '\n') try cooked.append(arena, '\r');
- try cooked.append(arena, ch);
+ if (ch == '\n') {
+ cooked[cooked_i] = '\r';
+ cooked_i += 1;
+ }
+ cooked[cooked_i] = ch;
+ cooked_i += 1;
}
- core.update(.{ .output = .{ .pane = 0, .bytes = cooked.items } });
+ core.update(.{ .output = .{ .pane = 0, .bytes = cooked } });
pump(core);
// leave tty mode the way a live session does: Ctrl-<tty_toggle>,
// then park the cursor at (0,0) — the helix harness's initial
@@ -400,12 +437,19 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
// its line list is exactly a file content's — join it and you have the
// same bytes, trailing newline included, with nothing to patch up.
const pl = try core.paneCursorLines(pane);
- var buf: std.ArrayList(u8) = .empty;
+ var text_len: usize = if (pl.lines.len == 0) 0 else pl.lines.len - 1;
+ for (pl.lines) |ln| text_len = std.math.add(usize, text_len, ln.len) catch return error.OutOfMemory;
+ const joined = try arena.alloc(u8, text_len);
+ var offset: usize = 0;
for (pl.lines, 0..) |ln, i| {
- if (i > 0) try buf.append(arena, '\n');
- try buf.appendSlice(arena, ln);
+ if (i > 0) {
+ joined[offset] = '\n';
+ offset += 1;
+ }
+ @memcpy(joined[offset..][0..ln.len], ln);
+ offset += ln.len;
}
- text = buf.items;
+ text = joined;
}
var anchor: Pos = .{ .row = pane.cur_row, .col = pane.cur_col };
if (pane.vsel.active) {
@@ -423,7 +467,8 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
var sels: ?[]const Sel = null;
var primary: ?usize = null;
if (pane.nsel > 0) {
- var out: std.ArrayList(Sel) = .empty;
+ const out = try arena.alloc(Sel, @as(usize, pane.nsel) + 1);
+ var out_len: usize = 0;
const prim: Sel = .{ .cursor = .{ .row = pane.cur_row, .col = pane.cur_col }, .anchor = anchor };
const pr_row = @min(prim.cursor.row, prim.anchor.row);
const pr_col = if (prim.cursor.row == prim.anchor.row) @min(prim.cursor.col, prim.anchor.col) else if (prim.cursor.row < prim.anchor.row) prim.cursor.col else prim.anchor.col;
@@ -431,16 +476,18 @@ fn runCase(arena: std.mem.Allocator, case: Case, pane_kind: []const u8) !Result
const s_row = @min(s.row, s.arow);
const s_col = if (s.row == s.arow) @min(s.col, s.acol) else if (s.row < s.arow) s.col else s.acol;
if (primary == null and (pr_row < s_row or (pr_row == s_row and pr_col <= s_col))) {
- primary = out.items.len;
- try out.append(arena, prim);
+ primary = out_len;
+ out[out_len] = prim;
+ out_len += 1;
}
- try out.append(arena, .{ .cursor = .{ .row = s.row, .col = s.col }, .anchor = .{ .row = s.arow, .col = s.acol } });
+ out[out_len] = .{ .cursor = .{ .row = s.row, .col = s.col }, .anchor = .{ .row = s.arow, .col = s.acol } };
+ out_len += 1;
}
if (primary == null) {
- primary = out.items.len;
- try out.append(arena, prim);
+ primary = out_len;
+ out[out_len] = prim;
}
- sels = out.items;
+ sels = out;
}
return .{
.name = case.name,
@@ -464,18 +511,30 @@ fn readFileAlloc(arena: std.mem.Allocator, path: []const u8) ![]u8 {
const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY });
if (fd < 0) return error.OpenFailed;
defer _ = libc.close(fd);
- var buf: std.ArrayList(u8) = .empty;
- var chunk: [16384]u8 = undefined;
+ const end = libc.lseek(fd, 0, libc.SEEK.END);
+ if (end < 0 or libc.lseek(fd, 0, libc.SEEK.SET) < 0) return error.StatFailed;
+ const size = std.math.cast(usize, end) orelse return error.FileTooLarge;
+ const buf = try arena.alloc(u8, size);
+ var offset: usize = 0;
+ while (offset < buf.len) {
+ const n = libc.read(fd, buf[offset..].ptr, buf.len - offset);
+ if (n < 0) {
+ if (libc.errno(n) == .INTR) continue;
+ return error.ReadFailed;
+ }
+ if (n == 0) return buf[0..offset];
+ offset += @intCast(n);
+ }
+ var extra: [1]u8 = undefined;
while (true) {
- const n = libc.read(fd, &chunk, chunk.len);
+ const n = libc.read(fd, &extra, extra.len);
if (n < 0) {
if (libc.errno(n) == .INTR) continue;
return error.ReadFailed;
}
- if (n == 0) break;
- try buf.appendSlice(arena, chunk[0..@intCast(n)]);
+ if (n > 0) return error.FileChanged;
+ return buf;
}
- return buf.items;
}
fn fatal(comptime fmt: []const u8, args: anytype) noreturn {