53b4f60da1
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
223 lines
11 KiB
Swift
223 lines
11 KiB
Swift
import Foundation
|
||
|
||
@main
|
||
enum DiffEngineTests {
|
||
|
||
static var failures = 0
|
||
|
||
static func checkTrue(_ label: String, _ value: Bool) {
|
||
if value { print(" ✓ \(label)") }
|
||
else { print(" ✗ \(label)"); failures += 1 }
|
||
}
|
||
|
||
static func checkInt(_ label: String, _ got: Int, _ expected: Int) {
|
||
if got == expected { print(" ✓ \(label)") }
|
||
else { print(" ✗ \(label) got: \(got) expected: \(expected)"); failures += 1 }
|
||
}
|
||
|
||
static func main() {
|
||
|
||
// ── DiffEngine.fromEdit — additions ───────────────────────────────────
|
||
print("DiffEngine.fromEdit — additions")
|
||
do {
|
||
let d = DiffEngine.fromEdit(old: "", new: "hello\nworld\n", path: "/a/b.swift")
|
||
checkInt("added == 2", d.added, 2)
|
||
checkInt("removed == 0", d.removed, 0)
|
||
checkTrue("!tooLarge", !d.tooLarge)
|
||
checkTrue("1 hunk", d.hunks.count == 1)
|
||
checkTrue("name == b.swift", d.name == "b.swift")
|
||
}
|
||
|
||
// ── DiffEngine.fromEdit — removals ────────────────────────────────────
|
||
print("DiffEngine.fromEdit — removals")
|
||
do {
|
||
let d = DiffEngine.fromEdit(old: "hello\nworld\n", new: "", path: "/x.py")
|
||
checkInt("added == 0", d.added, 0)
|
||
checkInt("removed == 2", d.removed, 2)
|
||
checkTrue("1 hunk", d.hunks.count == 1)
|
||
}
|
||
|
||
// ── DiffEngine.fromEdit — replacement ─────────────────────────────────
|
||
print("DiffEngine.fromEdit — replacement")
|
||
do {
|
||
let d = DiffEngine.fromEdit(old: "foo\nbar\nbaz\n",
|
||
new: "foo\nqux\nbaz\n",
|
||
path: "/f.ts")
|
||
checkInt("added == 1", d.added, 1)
|
||
checkInt("removed == 1", d.removed, 1)
|
||
checkTrue("has context lines",
|
||
d.hunks.first?.lines.contains(where: { $0.kind == .context }) == true)
|
||
}
|
||
|
||
// ── DiffEngine.fromNew — new file ──────────────────────────────────────
|
||
print("DiffEngine.fromNew — new file")
|
||
do {
|
||
let d = DiffEngine.fromNew(content: "line1\nline2\nline3\n", path: "/new.rs")
|
||
checkInt("added == 3", d.added, 3)
|
||
checkInt("removed == 0", d.removed, 0)
|
||
checkTrue("all lines .added",
|
||
d.hunks.flatMap { $0.lines }.allSatisfy { $0.kind == .added })
|
||
}
|
||
|
||
// ── DiffEngine.fromEdit — MultiEdit (two edits) ────────────────────────
|
||
print("DiffEngine.fromEdit — MultiEdit (two edits)")
|
||
do {
|
||
let d1 = DiffEngine.fromEdit(old: "aaa\n", new: "bbb\n", path: "/m.kt")
|
||
let d2 = DiffEngine.fromEdit(old: "ccc\n", new: "ddd\n", path: "/m.kt")
|
||
checkInt("d1 added == 1", d1.added, 1)
|
||
checkInt("d1 removed == 1", d1.removed, 1)
|
||
checkInt("d2 added == 1", d2.added, 1)
|
||
checkInt("d2 removed == 1", d2.removed, 1)
|
||
}
|
||
|
||
// ── DiffEngine — tooLarge ──────────────────────────────────────────────
|
||
print("DiffEngine — tooLarge")
|
||
do {
|
||
// Generate > 200 KB combined content
|
||
let bigOld = String(repeating: "x", count: 150 * 1024)
|
||
let bigNew = String(repeating: "y", count: 60 * 1024)
|
||
let d = DiffEngine.fromEdit(old: bigOld, new: bigNew, path: "/big.swift")
|
||
checkTrue("tooLarge == true", d.tooLarge)
|
||
checkTrue("hunks.isEmpty", d.hunks.isEmpty)
|
||
}
|
||
|
||
// ── DiffEngine — CRLF ─────────────────────────────────────────────────
|
||
print("DiffEngine — CRLF")
|
||
do {
|
||
let d = DiffEngine.fromEdit(old: "a\r\nb\r\n", new: "a\r\nc\r\n", path: "/win.txt")
|
||
checkInt("added == 1", d.added, 1)
|
||
checkInt("removed == 1", d.removed, 1)
|
||
}
|
||
|
||
// ── DiffEngine — no trailing newline ──────────────────────────────────
|
||
print("DiffEngine — no trailing newline")
|
||
do {
|
||
let d = DiffEngine.fromEdit(old: "hello", new: "hello\nworld", path: "/t.txt")
|
||
checkInt("added == 1", d.added, 1)
|
||
checkInt("removed == 0", d.removed, 0)
|
||
}
|
||
|
||
// ── DiffEngine — 3-line context ───────────────────────────────────────
|
||
print("DiffEngine — 3-line context")
|
||
do {
|
||
// 10-line file, change line 5
|
||
let oldContent = (1...10).map { "line\($0)" }.joined(separator: "\n") + "\n"
|
||
let newContent = (1...10).map { $0 == 5 ? "changed" : "line\($0)" }.joined(separator: "\n") + "\n"
|
||
let d = DiffEngine.fromEdit(old: oldContent, new: newContent, path: "/ctx.swift")
|
||
checkInt("added == 1", d.added, 1)
|
||
checkInt("removed == 1", d.removed, 1)
|
||
checkTrue("1 hunk", d.hunks.count == 1)
|
||
let hunk = d.hunks[0]
|
||
let contextLines = hunk.lines.filter { $0.kind == .context }
|
||
checkTrue("has context before change", contextLines.count >= 3)
|
||
}
|
||
|
||
// ── DiffEngine — m*n > 1_000_000 → tooLarge ─────────────────────────
|
||
print("DiffEngine — m*n > 1_000_000 → tooLarge")
|
||
do {
|
||
// 1001 old lines × 1001 new lines = > 1M
|
||
let many = (0..<1001).map { "line\($0)" }.joined(separator: "\n")
|
||
let d = DiffEngine.fromEdit(old: many, new: many + "\nextra", path: "/big.swift")
|
||
checkTrue("tooLarge when m*n > 1M", d.tooLarge)
|
||
checkTrue("hunks empty", d.hunks.isEmpty)
|
||
}
|
||
|
||
// ── DiffEngine.fromNew — too large ────────────────────────────────────
|
||
print("DiffEngine.fromNew — too large")
|
||
do {
|
||
let bigContent = String(repeating: "x\n", count: FileDiff.maxLines + 1)
|
||
let d = DiffEngine.fromNew(content: bigContent, path: "/new.swift")
|
||
checkTrue("fromNew tooLarge", d.tooLarge)
|
||
checkTrue("fromNew isNewFile", d.isNewFile)
|
||
checkTrue("fromNew hunks empty", d.hunks.isEmpty)
|
||
checkTrue("fromNew added > 0", d.added > 0)
|
||
}
|
||
|
||
// ── String.makeDiffStep / parseDiffStep ───────────────────────────────
|
||
print("String.makeDiffStep / parseDiffStep")
|
||
do {
|
||
let s = String.makeDiffStep(filename: "foo.swift", added: 3, removed: 1, diffId: 7)
|
||
checkTrue("isDiffStep", s.isDiffStep)
|
||
let parsed = s.parseDiffStep()
|
||
checkTrue("parsed != nil", parsed != nil)
|
||
checkTrue("filename round-trips", parsed?.filename == "foo.swift")
|
||
checkTrue("added round-trips", parsed?.added == 3)
|
||
checkTrue("removed round-trips", parsed?.removed == 1)
|
||
checkTrue("diffId round-trips", parsed?.diffId == 7)
|
||
|
||
// id > 9 (multi-digit) round-trips correctly
|
||
let s2 = String.makeDiffStep(filename: "bar.ts", added: 0, removed: 2, diffId: 42)
|
||
checkTrue("diffId 42 round-trips", s2.parseDiffStep()?.diffId == 42)
|
||
|
||
// Non-diff step should not parse
|
||
checkTrue("normal step !isDiffStep", !"Edit foo.swift".isDiffStep)
|
||
checkTrue("normal step parseDiffStep == nil", "Edit foo.swift".parseDiffStep() == nil)
|
||
}
|
||
|
||
// ── DiffEngine.toOneLine ──────────────────────────────────────────────
|
||
print("DiffEngine.toOneLine")
|
||
do {
|
||
// multi-line joined with space
|
||
checkTrue("multi-line joined",
|
||
DiffEngine.toOneLine("line one\nline two\nline three") == "line one line two line three")
|
||
|
||
// bold stripped
|
||
checkTrue("bold stripped",
|
||
DiffEngine.toOneLine("**hello** world") == "hello world")
|
||
|
||
// heading stripped
|
||
checkTrue("heading stripped",
|
||
DiffEngine.toOneLine("## My Title\nsome text") == "My Title some text")
|
||
|
||
// empty input → empty
|
||
checkTrue("empty → empty", DiffEngine.toOneLine("").isEmpty)
|
||
|
||
// truncation
|
||
let long = DiffEngine.toOneLine(String(repeating: "x ", count: 200), maxChars: 10)
|
||
checkTrue("truncated to maxChars", long.count <= 10)
|
||
|
||
// stop at blank line
|
||
checkTrue("blank line → first para only",
|
||
DiffEngine.toOneLine("First para.\n\nSecond para.") == "First para.")
|
||
|
||
// stop at --- separator
|
||
checkTrue("--- separator → first para only",
|
||
DiffEngine.toOneLine("Done. Single commit 450a657 on github-pulse.\n\n---\n\nFiles touched (7)…")
|
||
== "Done. Single commit 450a657 on github-pulse.")
|
||
|
||
// stop at *** separator
|
||
checkTrue("*** separator → first para only",
|
||
DiffEngine.toOneLine("Summary line.\n***\nMore details.") == "Summary line.")
|
||
|
||
// stop at table row (|)
|
||
checkTrue("table row → first para only",
|
||
DiffEngine.toOneLine("Result:\n| Col1 | Col2 |\n|---|---|\n| A | B |") == "Result:")
|
||
|
||
// strip leading bullet -
|
||
checkTrue("strip bullet -",
|
||
DiffEngine.toOneLine("- item one\n- item two") == "item one item two")
|
||
|
||
// strip leading bullet *
|
||
checkTrue("strip bullet *",
|
||
DiffEngine.toOneLine("* first\n* second") == "first second")
|
||
|
||
// strip ordered list
|
||
checkTrue("strip ordered list",
|
||
DiffEngine.toOneLine("1. step one\n2. step two") == "step one step two")
|
||
|
||
// first paragraph empty → fall through to next
|
||
checkTrue("empty first para → next",
|
||
DiffEngine.toOneLine("\n\nActual content.") == "Actual content.")
|
||
}
|
||
|
||
// ── finish ─────────────────────────────────────────────────────────────
|
||
if failures == 0 {
|
||
print("\nAll tests passed.")
|
||
exit(0)
|
||
} else {
|
||
print("\n\(failures) test(s) failed.")
|
||
exit(1)
|
||
}
|
||
}
|
||
}
|