Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! Tests of the line diff, its hunks and patches: what was found by//! experiment, pinned, and patches compared with `git diff --no-index`//! over generated texts, under SHA-1 and SHA-256.
const std = @import("std");const testing = std.testing;const Io = std.Io;
const LineDiff = @import("LineDiff.zig");const hunks = @import("hunks.zig");const indent = @import("indent.zig");const patch = @import("patch.zig");const fixture = @import("../testing/fixture.zig");const fuzz_input = @import("../testing/fuzz.zig");
test "lines split with and without a final newline" { const l = try LineDiff.splitLines(testing.allocator, "a\nb"); defer testing.allocator.free(l.starts); try testing.expectEqual(@as(usize, 2), l.len()); try testing.expectEqualStrings("b", l.line(1)); try testing.expect(l.missingNewline());}
fn sameKey(x: []const u8, y: []const u8, ws: LineDiff.Whitespace) !bool { var kx: std.ArrayList(u8) = .empty; defer kx.deinit(testing.allocator); var ky: std.ArrayList(u8) = .empty; defer ky.deinit(testing.allocator); try LineDiff.comparisonKey(x, ws, &kx, testing.allocator); try LineDiff.comparisonKey(y, ws, &ky, testing.allocator); return std.mem.eql(u8, kx.items, ky.items);}
test "lines compared under the whitespace options as git compares them" { const w: LineDiff.Whitespace = .{ .ignore_all = true }; const b: LineDiff.Whitespace = .{ .ignore_change = true }; const eol: LineDiff.Whitespace = .{ .ignore_at_eol = true }; const cr: LineDiff.Whitespace = .{ .ignore_cr_at_eol = true }; // With any option, the newline itself is not compared. for ([_]LineDiff.Whitespace{ w, b, eol, cr }) |ws| try testing.expect(try sameKey("a\n", "a", ws)); try testing.expect(!try sameKey("a\n", "a", .{})); // Space, tab and carriage return are blank space; \v and \f are not. try testing.expect(try sameKey("a \t\r\n", "a\n", eol)); try testing.expect(!try sameKey("a\x0b\n", "a\n", w)); try testing.expect(!try sameKey("a\x0c\n", "a\n", w)); try testing.expect(try sameKey(" a b\n", "ab\n", w)); try testing.expect(!try sameKey(" a\n", "a\n", b)); try testing.expect(try sameKey("a\tb \n", "a b\n", b)); try testing.expect(!try sameKey("a b\n", "ab\n", b)); // One carriage return before the newline, and only before one. try testing.expect(try sameKey("a\r\n", "a", cr)); try testing.expect(!try sameKey("a\r", "a", cr)); try testing.expect(!try sameKey("a\r\r\n", "a\n", cr)); try testing.expect(!try sameKey("a \n", "a\n", cr));}
test "the limits found by experiment" { // Provisional lines: 4 for texts of 8 to 15 lines, 8 to 63, 16 to 255. try testing.expectEqual(@as(usize, 4), LineDiff.frequentLimit(8)); try testing.expectEqual(@as(usize, 4), LineDiff.frequentLimit(15)); try testing.expectEqual(@as(usize, 8), LineDiff.frequentLimit(16)); try testing.expectEqual(@as(usize, 8), LineDiff.frequentLimit(63)); try testing.expectEqual(@as(usize, 16), LineDiff.frequentLimit(64)); try testing.expectEqual(@as(usize, 32), LineDiff.frequentLimit(1023)); // The search for a middle snake: 256 up to 2^16 lines, then doubling // every factor of four, git counting three lines more than there are. try testing.expectEqual(@as(usize, 256), LineDiff.costLimit(1000)); try testing.expectEqual(@as(usize, 256), LineDiff.costLimit(65532)); try testing.expectEqual(@as(usize, 512), LineDiff.costLimit(65533)); try testing.expectEqual(@as(usize, 512), LineDiff.costLimit(262140)); try testing.expectEqual(@as(usize, 1024), LineDiff.costLimit(262141));}
test "function names as git's default rule finds and cuts them" { try testing.expectEqualStrings("func", hunks.defaultFunctionName("func\n").?); try testing.expectEqualStrings("$x", hunks.defaultFunctionName("$x\n").?); try testing.expect(hunks.defaultFunctionName(" sp\n") == null); try testing.expect(hunks.defaultFunctionName("9num\n") == null); try testing.expectEqualStrings("f", hunks.defaultFunctionName("f \t\r\n").?); try testing.expectEqualStrings("f\x0c", hunks.defaultFunctionName("f\x0c \n").?); const long = "f" ++ @as([100]u8, @splat('a')); try testing.expectEqual(@as(usize, 80), hunks.defaultFunctionName(long).?.len); // Cut inside a UTF-8 sequence, whose start is then not written. const utf = "f" ++ @as([100]u8, @bitCast(@as([50][2]u8, @splat("\xc3\xa9".*)))); try testing.expectEqual(@as(usize, 80), hunks.defaultFunctionName(utf).?.len); try testing.expectEqual(@as(usize, 79), hunks.shownFunctionName(hunks.defaultFunctionName(utf).?).len); // What git 2.56 wrote: up to the first byte that is not UTF-8, blank // space before it kept. try testing.expectEqualStrings("ab ", hunks.shownFunctionName(hunks.defaultFunctionName("ab \xe9 \n").?)); try testing.expectEqualStrings("ab\t", hunks.shownFunctionName(hunks.defaultFunctionName("ab\t\xe9cd\n").?)); try testing.expectEqualStrings("ab", hunks.shownFunctionName("ab\xed\xa0\x80cd")); try testing.expectEqualStrings("ab", hunks.shownFunctionName("ab\xc0\xafcd")); try testing.expectEqualStrings("ab", hunks.shownFunctionName("ab\xef\xbf\xbfcd")); try testing.expectEqualStrings("ab\xef\xb7\x90cd", hunks.shownFunctionName("ab\xef\xb7\x90cd")); try testing.expectEqualStrings("ab", hunks.shownFunctionName("ab\xf4\x90\x80\x80cd")); try testing.expectEqualStrings("ab\xf0\x9f\x98\x80cd", hunks.shownFunctionName("ab\xf0\x9f\x98\x80cd"));}
/// A generator of texts with the shapes that find git's choices: small/// alphabets of repeated lines, code-like lines with blank ones, lines/// differing only in blank space, missing final newlines.const Gen = struct { rng: std.Random, buf: std.ArrayList(u8) = .empty,
const words = [_][]const u8{ "", "}", "{", "x", "y", "return 0;", " if (a) {", " b();", " }", "int f(void)", "\tfoo", "bar ", "baz\t", "# c", "a b c" };
fn text(g: *Gen, gpa: std.mem.Allocator, ls: []const []const u8) ![]u8 { g.buf.clearRetainingCapacity(); const crlf = g.rng.uintLessThan(u8, 10) == 0; for (ls, 0..) |l, i| { try g.buf.appendSlice(gpa, l); if (i + 1 < ls.len or g.rng.uintLessThan(u8, 8) != 0) try g.buf.appendSlice(gpa, if (crlf) "\r\n" else "\n"); } return gpa.dupe(u8, g.buf.items); }
fn lines(g: *Gen, gpa: std.mem.Allocator, out: *std.ArrayList([]const u8)) !void { out.clearRetainingCapacity(); const kind = g.rng.uintLessThan(u8, 3); const n = g.rng.uintLessThan(usize, 30); const letters = [_][]const u8{ "a", "b", "c", "d" }; for (0..n) |_| { try out.append(gpa, switch (kind) { 0 => letters[g.rng.uintLessThan(usize, 2 + g.rng.uintLessThan(usize, 3))], else => words[g.rng.uintLessThan(usize, words.len)], }); } }
const code_words = [_][]const u8{ "x = 1;", "return y;", "foo();", "bar(x);", "# note", "end" }; const openers = [_][]const u8{ "if (a) {", "for (;;) {", "def f():", "while (x) {" }; const closers = [_][]const u8{ "}", "end", "};" }; const units = [_][]const u8{ " ", " ", "\t", " " }; const blanks = [_][]const u8{ "", "", "", " ", "\t" };
/// Code-like lines: nested blocks indented by one unit, blank and /// whitespace-only lines between them. The lines are kept in `arena`. fn code(g: *Gen, arena: std.mem.Allocator, out: *std.ArrayList([]const u8)) !void { out.clearRetainingCapacity(); const n = g.rng.uintLessThan(usize, 40); const unit = units[g.rng.uintLessThan(usize, units.len)]; var depth: usize = 0; for (0..n) |_| { const r = g.rng.uintLessThan(u8, 20); if (r < 4) { try out.append(arena, blanks[g.rng.uintLessThan(usize, blanks.len)]); continue; } var line: std.ArrayList(u8) = .empty; if (r < 7 and depth > 0) depth -= 1; for (0..depth) |_| try line.appendSlice(arena, unit); if (r < 7) { try line.appendSlice(arena, closers[g.rng.uintLessThan(usize, closers.len)]); } else if (r < 10 and depth < 5) { try line.appendSlice(arena, openers[g.rng.uintLessThan(usize, openers.len)]); depth += 1; } else { try line.appendSlice(arena, code_words[g.rng.uintLessThan(usize, code_words.len)]); } try out.append(arena, line.items); } }
/// `from` with a block of it repeated in place, so that the added copy /// can slide, and perhaps one more edit. fn repeatBlock(g: *Gen, gpa: std.mem.Allocator, from: []const []const u8, out: *std.ArrayList([]const u8)) !void { out.clearRetainingCapacity(); try out.appendSlice(gpa, from); if (from.len == 0) return; const i = g.rng.uintLessThan(usize, from.len); const len = @min(from.len - i, 1 + g.rng.uintLessThan(usize, 6)); try out.insertSlice(gpa, i + len, from[i..][0..len]); if (g.rng.boolean() and out.items.len != 0) { const j = g.rng.uintLessThan(usize, out.items.len); _ = out.orderedRemove(j); } }
fn mutate(g: *Gen, gpa: std.mem.Allocator, from: []const []const u8, out: *std.ArrayList([]const u8)) !void { out.clearRetainingCapacity(); try out.appendSlice(gpa, from); const edits = g.rng.uintLessThan(usize, 6); for (0..edits) |_| { const r = g.rng.uintLessThan(u8, 4); if (r == 0 and out.items.len != 0) { const i = g.rng.uintLessThan(usize, out.items.len); _ = out.orderedRemove(i); } else if (r == 1) { const i = g.rng.uintAtMost(usize, out.items.len); try out.insert(gpa, i, words[g.rng.uintLessThan(usize, words.len)]); } else if (r == 2 and out.items.len != 0) { const i = g.rng.uintLessThan(usize, out.items.len); try out.insert(gpa, i, out.items[i]); } else if (out.items.len != 0) { const i = g.rng.uintLessThan(usize, out.items.len); out.items[i] = words[g.rng.uintLessThan(usize, words.len)]; } } }};
const option_sets = [_][]const []const u8{ &.{}, &.{"-w"}, &.{"-b"}, &.{"--ignore-space-at-eol"}, &.{"--ignore-cr-at-eol"}, &.{"--ignore-blank-lines"}, &.{"-U0"}, &.{"-U1"}, &.{"--inter-hunk-context=2"}, &.{"--minimal"}, &.{ "-U0", "-w" }, &.{ "--ignore-blank-lines", "-U1" },};
fn applyOptions(args: []const []const u8, o: *patch.Options) void { for (args) |a| { if (std.mem.eql(u8, a, "-w")) o.lines.whitespace.ignore_all = true; if (std.mem.eql(u8, a, "-b")) o.lines.whitespace.ignore_change = true; if (std.mem.eql(u8, a, "--ignore-space-at-eol")) o.lines.whitespace.ignore_at_eol = true; if (std.mem.eql(u8, a, "--ignore-cr-at-eol")) o.lines.whitespace.ignore_cr_at_eol = true; if (std.mem.eql(u8, a, "--ignore-blank-lines")) o.hunks.ignore_blank_lines = true; if (std.mem.eql(u8, a, "-U0")) o.hunks.context = 0; if (std.mem.eql(u8, a, "-U1")) o.hunks.context = 1; if (std.mem.eql(u8, a, "--inter-hunk-context=2")) o.hunks.inter_hunk_context = 2; if (std.mem.eql(u8, a, "--minimal")) o.lines.algorithm = .minimal; }}
fn compareGenerated(seed: u64, cases: usize, heuristic: bool) !void { const gpa = testing.allocator; var repo = try fixture.Repo.init(gpa, testing.io); defer repo.deinit(); var arena_state: std.heap.ArenaAllocator = .init(gpa); defer arena_state.deinit(); var prng: std.Random.DefaultPrng = .init(seed); var g: Gen = .{ .rng = prng.random() }; defer g.buf.deinit(gpa); var la: std.ArrayList([]const u8) = .empty; defer la.deinit(gpa); var lb: std.ArrayList([]const u8) = .empty; defer lb.deinit(gpa); for (0..cases) |case| { _ = arena_state.reset(.retain_capacity); const shape = g.rng.uintLessThan(u8, 3); if (heuristic and shape != 0) { try g.code(arena_state.allocator(), &la); if (shape == 1) try g.repeatBlock(gpa, la.items, &lb) else try g.mutate(gpa, la.items, &lb); } else { try g.lines(gpa, &la); if (g.rng.uintLessThan(u8, 5) == 0) try g.lines(gpa, &lb) else try g.mutate(gpa, la.items, &lb); } const old = try g.text(gpa, la.items); defer gpa.free(old); const new = try g.text(gpa, lb.items); defer gpa.free(new); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = "old", .data = old }); try repo.tmp.dir.writeFile(repo.io, .{ .sub_path = "new", .data = new }); const opts = option_sets[case % option_sets.len]; var args: std.ArrayList([]const u8) = .empty; defer args.deinit(gpa); try args.appendSlice(gpa, &.{ "diff", "--no-index", if (heuristic) "--indent-heuristic" else "--no-indent-heuristic" }); try args.appendSlice(gpa, opts); try args.appendSlice(gpa, &.{ "old", "new" }); const theirs = try repo.runIn(".", args.items, ""); defer theirs.deinit(gpa);
var options: patch.Options = .{ .object_format = repo.object_format }; options.lines.indent_heuristic = heuristic; applyOptions(opts, &options); var out: Io.Writer.Allocating = .init(gpa); defer out.deinit(); const outcome = try patch.write(gpa, &out.writer, .{ .path = "old", .contents = old }, .{ .path = "new", .contents = new }, options); errdefer std.debug.print("case {d}: options {d}\nold:\n{s}\nnew:\n{s}\n", .{ case, case % option_sets.len, old, new }); try testing.expectEqualStrings(theirs.stdout, out.written()); try testing.expectEqual(theirs.code == 1, outcome.differs); }}
test "patches are byte-identical to git diff --no-index over generated texts" { try compareGenerated(0x5eed, 120, false);}
test "patches with the indent heuristic are byte-identical to git's over generated texts" { try compareGenerated(0x1d3a7, 240, true);}
test "the cost limit gives up as git does on a block moved past another" { // a = P Q, b = Q P with P one line longer: git keeps Q, a shortest // script keeps P. 258 lines each way makes the search reach 257 steps. const gpa = testing.allocator; var old: std.ArrayList(u8) = .empty; defer old.deinit(gpa); var new: std.ArrayList(u8) = .empty; defer new.deinit(gpa); var p: std.ArrayList(u8) = .empty; defer p.deinit(gpa); var q: std.ArrayList(u8) = .empty; defer q.deinit(gpa); for (0..258) |i| try p.print(gpa, "p{d}\n", .{i}); for (0..257) |i| try q.print(gpa, "q{d}\n", .{i}); try old.appendSlice(gpa, p.items); try old.appendSlice(gpa, q.items); try new.appendSlice(gpa, q.items); try new.appendSlice(gpa, p.items); var d = try LineDiff.compute(gpa, old.items, new.items, .{ .indent_heuristic = false }); defer d.deinit(gpa); var changed: usize = 0; for (d.a_changed) |c| changed += @intFromBool(c); try testing.expectEqual(@as(usize, 258), changed); var m = try LineDiff.compute(gpa, old.items, new.items, .{ .algorithm = .minimal, .indent_heuristic = false }); defer m.deinit(gpa); changed = 0; for (m.a_changed) |c| changed += @intFromBool(c); try testing.expectEqual(@as(usize, 257), changed);}
// -- fuzzing -------------------------------------------------------------------
/// Splits a fuzz input into options and two texts: the first byte picks/// the options, and the rest divides at the first 0xff.fn splitInput(input: []const u8) struct { u8, []const u8, []const u8 } { if (input.len == 0) return .{ 0, "", "" }; const rest = input[1..]; const cut = std.mem.findScalar(u8, rest, 0xff) orelse rest.len; return .{ input[0], rest[0..cut], if (cut < rest.len) rest[cut + 1 ..] else "" };}
fn lineOptions(flags: u8) LineDiff.Options { return .{ .algorithm = if (flags & 1 != 0) .minimal else .myers, .whitespace = .{ .ignore_all = flags & 2 != 0, .ignore_change = flags & 4 != 0, .ignore_at_eol = flags & 8 != 0, .ignore_cr_at_eol = flags & 16 != 0, }, .indent_heuristic = false, };}
fn countTrue(flags: []const bool) usize { var n: usize = 0; for (flags) |f| n += @intFromBool(f); return n;}
/// A valid edit script: the unchanged lines, taken in order, pair lines/// that compare equal, and all of each text is accounted for.fn expectValidScript(gpa: std.mem.Allocator, d: *const LineDiff, ws: LineDiff.Whitespace) !void { var ka: std.ArrayList(u8) = .empty; defer ka.deinit(gpa); var kb: std.ArrayList(u8) = .empty; defer kb.deinit(gpa); var i: usize = 0; var j: usize = 0; while (true) { while (i < d.a_changed.len and d.a_changed[i]) i += 1; while (j < d.b_changed.len and d.b_changed[j]) j += 1; if (i == d.a_changed.len or j == d.b_changed.len) { try testing.expect(i == d.a_changed.len and j == d.b_changed.len); break; } try LineDiff.comparisonKey(d.a.line(i), ws, &ka, gpa); try LineDiff.comparisonKey(d.b.line(j), ws, &kb, gpa); try testing.expectEqualStrings(ka.items, kb.items); i += 1; j += 1; }}
/// a = A B C, b = C B A, with `half` lines in each and C of `k` lines:/// a shortest script keeps A, which takes a search of about `k` steps.fn blockSwap(gpa: std.mem.Allocator, half: usize, k: usize, old: *std.ArrayList(u8), new: *std.ArrayList(u8)) !void { var a_part: std.ArrayList(u8) = .empty; defer a_part.deinit(gpa); var c_part: std.ArrayList(u8) = .empty; defer c_part.deinit(gpa); for (0..half - k - 3) |i| try a_part.print(gpa, "a{d}\n", .{i}); for (0..k) |i| try c_part.print(gpa, "c{d}\n", .{i}); const b_part = "b0\nb1\nb2\n"; try old.appendSlice(gpa, a_part.items); try old.appendSlice(gpa, b_part); try old.appendSlice(gpa, c_part.items); try new.appendSlice(gpa, c_part.items); try new.appendSlice(gpa, b_part); try new.appendSlice(gpa, a_part.items);}
test "a search for a middle snake past 256 steps is refused" { // 65,534 lines search to 512 steps, where git may split a box early // by a rule only partly found (the case that showed it, harness seed // 302, was 67,479 lines). Below that, 256 steps and git's give-up. const gpa = testing.allocator; var old: std.ArrayList(u8) = .empty; defer old.deinit(gpa); var new: std.ArrayList(u8) = .empty; defer new.deinit(gpa); try blockSwap(gpa, 32767, 300, &old, &new); try testing.expectError(error.LongSearchUnsupported, LineDiff.compute(gpa, old.items, new.items, .{ .indent_heuristic = false })); try testing.expectError(error.LongSearchUnsupported, LineDiff.compute(gpa, old.items, new.items, .{})); // --minimal never gives up, and keeps A. var m = try LineDiff.compute(gpa, old.items, new.items, .{ .algorithm = .minimal, .indent_heuristic = false }); defer m.deinit(gpa); try testing.expectEqual(@as(usize, 303), countTrue(m.a_changed));
// Two lines fewer: git gives up at 256 steps and keeps only C. old.clearRetainingCapacity(); new.clearRetainingCapacity(); try blockSwap(gpa, 32766, 300, &old, &new); var d = try LineDiff.compute(gpa, old.items, new.items, .{ .indent_heuristic = false }); defer d.deinit(gpa); try testing.expectEqual(@as(usize, 32766 - 300), countTrue(d.a_changed));}
fn fuzzEngine(_: void, smith: *testing.Smith) anyerror!void { var buffer: [4096]u8 = undefined; const flags, const old, const new = splitInput(fuzz_input.bytes(smith, &buffer)); const gpa = testing.allocator; const options = lineOptions(flags); var d = try LineDiff.compute(gpa, old, new, options); defer d.deinit(gpa); try expectValidScript(gpa, &d, options.whitespace); // The indent heuristic only moves runs: still a valid edit script, // with as many changes as without it. Setting the common tail aside // (`-U0`) compares less of the texts, so it is checked on its own. for ([_]bool{ false, true }) |trim| { var base_options = options; base_options.trim_tail = trim; var base = try LineDiff.compute(gpa, old, new, base_options); defer base.deinit(gpa); try expectValidScript(gpa, &base, options.whitespace); var h_options = base_options; h_options.indent_heuristic = true; var h = try LineDiff.compute(gpa, old, new, h_options); defer h.deinit(gpa); try expectValidScript(gpa, &h, options.whitespace); try testing.expectEqual(countTrue(base.a_changed), countTrue(h.a_changed)); try testing.expectEqual(countTrue(base.b_changed), countTrue(h.b_changed)); }}
test "fuzz: line diff engine" { try testing.fuzz({}, fuzzEngine, .{ .corpus = &.{ @embedFile("../fuzz/diff-pair"), @embedFile("../fuzz/diff-pair-blank"), @embedFile("../fuzz/diff-pair-repeats"), } });}
/// A text, and the same text with a block of its lines repeated in place/// so that the added copy can slide: the first byte of the input picks/// the block and the options, the rest is the text.fn fuzzIndent(_: void, smith: *testing.Smith) anyerror!void { var buffer: [4096]u8 = undefined; const input = fuzz_input.bytes(smith, &buffer); if (input.len == 0) return; const gpa = testing.allocator; const flags = input[0]; const old = input[1..]; const lines = try LineDiff.splitLines(gpa, old); defer gpa.free(lines.starts); if (lines.len() == 0) return; const at = (flags >> 3) % lines.len(); const len = @min(lines.len() - at, 1 + (flags & 7)); var new: std.ArrayList(u8) = .empty; defer new.deinit(gpa); try new.appendSlice(gpa, old[0..lines.starts[at + len]]); if (lines.missingNewline() and at + len == lines.len()) try new.append(gpa, '\n'); try new.appendSlice(gpa, old[lines.starts[at]..lines.starts[at + len]]); try new.appendSlice(gpa, old[lines.starts[at + len]..]); for ([_]bool{ false, true }) |trim| { var options: LineDiff.Options = .{ .indent_heuristic = false, .trim_tail = trim, .whitespace = .{ .ignore_all = flags & 0x80 != 0 } }; var plain = try LineDiff.compute(gpa, old, new.items, options); defer plain.deinit(gpa); options.indent_heuristic = true; var h = try LineDiff.compute(gpa, old, new.items, options); defer h.deinit(gpa); // Still a valid edit script, with no more changes than without // the heuristic: it only chooses where runs go. try expectValidScript(gpa, &h, options.whitespace); try testing.expectEqual(countTrue(plain.a_changed), countTrue(h.a_changed)); try testing.expectEqual(countTrue(plain.b_changed), countTrue(h.b_changed)); } // The placement chosen is one of the candidates. const indents = try gpa.alloc(?u32, lines.len()); defer gpa.free(indents); for (indents, 0..) |*v, i| v.* = indent.indentOf(lines.line(i)); const top = (flags >> 1) % lines.len(); const run = @min(lines.len() - top, 1 + (flags >> 6)); const bottom = top + (lines.len() - top - run) / 2; const c = indent.choose(&indent.git_weights, indents, top, bottom, run); try testing.expect(c >= top and c <= bottom and bottom - c <= @min(run + 1, indent.max_slide));}
test "fuzz: indent heuristic" { try testing.fuzz({}, fuzzIndent, .{ .corpus = &.{ @embedFile("../fuzz/indent-code"), @embedFile("../fuzz/indent-blank-runs"), @embedFile("../fuzz/indent-tabs"), @embedFile("../fuzz/diff-pair-blank"), } });}
fn fuzzHunks(_: void, smith: *testing.Smith) anyerror!void { var buffer: [4096]u8 = undefined; const flags, const old, const new = splitInput(fuzz_input.bytes(smith, &buffer)); const gpa = testing.allocator; var d = try LineDiff.compute(gpa, old, new, lineOptions(flags)); defer d.deinit(gpa); const cs = try hunks.changes(gpa, &d); defer gpa.free(cs); const options: hunks.Options = .{ .context = flags >> 5, .inter_hunk_context = (flags >> 3) & 3, .ignore_blank_lines = flags & 4 != 0, }; const hs = try hunks.build(gpa, &d, cs, options); defer gpa.free(hs); // Hunks are in order, apart, inside the texts, and each holds the // changes it says it does, with the same unchanged lines on each side. var prev_a_end: usize = 0; for (hs, 0..) |h, n| { try testing.expect(h.first < h.end and h.end <= cs.len); if (n > 0) try testing.expect(h.a_start > prev_a_end or (h.a_start == prev_a_end and hs[n - 1].end <= h.first)); try testing.expect(h.a_start + h.a_len <= d.a_changed.len); try testing.expect(h.b_start + h.b_len <= d.b_changed.len); var changed_a: usize = 0; for (d.a_changed[h.a_start..][0..h.a_len]) |c| changed_a += @intFromBool(c); var changed_b: usize = 0; for (d.b_changed[h.b_start..][0..h.b_len]) |c| changed_b += @intFromBool(c); try testing.expectEqual(h.a_len - changed_a, h.b_len - changed_b); prev_a_end = h.a_start + h.a_len; } // Without changes that may be left out, every change is in a hunk. if (!options.ignore_blank_lines) { var covered: usize = 0; for (hs) |h| covered += h.end - h.first; try testing.expectEqual(cs.len, covered); }}
test "fuzz: hunks" { try testing.fuzz({}, fuzzHunks, .{ .corpus = &.{ @embedFile("../fuzz/diff-pair"), @embedFile("../fuzz/diff-pair-blank"), @embedFile("../fuzz/diff-pair-repeats"), } });}