import { describe, expect, test } from "bun:test"; import { diffLines, hunks } from "../src/pages/diff.ts"; // Longest common subsequence by dynamic programming: the minimal diff keeps exactly this many lines. function lcs(a: string[], b: string[]): number { const row = new Array(b.length + 1).fill(0); for (const line of a) { let diagonal = 0; for (let j = 1; j <= b.length; j++) { const above = row[j] as number; row[j] = line === b[j - 1] ? diagonal + 1 : Math.max(above, row[j - 1] as number); diagonal = above; } } return row[b.length] as number; } // A small seeded generator, so every run tests the same inputs. function random(seed: number): () => number { let s = seed; return () => { s = (s * 1103515245 + 12345) % 2147483648; return s / 2147483648; }; } describe("diffLines", () => { test("rebuilds both sides and keeps a longest common subsequence", () => { const next = random(7); for (let round = 0; round < 300; round++) { const lines = (n: number) => Array.from({ length: n }, () => "abcd"[Math.floor(next() * 4)] as string); const a = lines(Math.floor(next() * 14)); const b = lines(Math.floor(next() * 14)); const edits = diffLines(a, b); expect(edits.filter((e) => e.kind !== "+").map((e) => a[e.a])).toEqual(a); expect(edits.filter((e) => e.kind !== "-").map((e) => b[e.b])).toEqual(b); for (const e of edits.filter((e) => e.kind === " ")) expect(a[e.a]).toBe(b[e.b] as string); expect(edits.filter((e) => e.kind === " ").length).toBe(lcs(a, b)); } }); test("an empty side is all additions or all removals", () => { expect(diffLines([], ["x", "y"]).map((e) => e.kind)).toEqual(["+", "+"]); expect(diffLines(["x"], []).map((e) => e.kind)).toEqual(["-"]); }); test("past the difference cap, a plain replacement still rebuilds both sides", () => { const a = Array.from({ length: 3000 }, (_, i) => `a${i}`); const b = ["same", ...Array.from({ length: 3000 }, (_, i) => `b${i}`), "end"]; a.unshift("same"); a.push("end"); const edits = diffLines(a, b); expect(edits.filter((e) => e.kind !== "+").map((e) => a[e.a])).toEqual(a); expect(edits.filter((e) => e.kind !== "-").map((e) => b[e.b])).toEqual(b); }); }); describe("hunks", () => { test("match GNU diff -u's headers and grouping", () => { const a = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`); const b = [...a]; b[1] = "changed 2"; b.splice(9, 1); b.push("line 21"); expect(hunks(diffLines(a, b)).map((h) => h.header)).toEqual([ "@@ -1,5 +1,5 @@", "@@ -7,7 +7,6 @@", "@@ -18,3 +17,4 @@", ]); // Changes six unchanged lines apart share one hunk; seven apart do not. const six = [...a]; six[2] = "x"; six[9] = "y"; expect(hunks(diffLines(a, six)).length).toBe(1); const seven = [...a]; seven[2] = "x"; seven[10] = "y"; expect(hunks(diffLines(a, seven)).length).toBe(2); expect(hunks(diffLines([], ["new"]))[0]?.header).toBe("@@ -0,0 +1 @@"); }); });