2026-06-04 02:29:23 -07:00
|
|
|
@testable import ForgejoKit
|
2026-02-28 11:10:51 -08:00
|
|
|
import Foundation
|
|
|
|
|
import Testing
|
|
|
|
|
|
|
|
|
|
struct DiffParserTests {
|
|
|
|
|
// MARK: - Empty and minimal input
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseEmptyString() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let result = DiffParser.parse("")
|
|
|
|
|
#expect(result.files.isEmpty)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseGarbageInput() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let result = DiffParser.parse("this is not a diff")
|
|
|
|
|
#expect(result.files.isEmpty)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Single file diff
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseSingleFileAddition() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/hello.txt b/hello.txt",
|
|
|
|
|
"new file mode 100644",
|
|
|
|
|
"--- /dev/null",
|
|
|
|
|
"+++ b/hello.txt",
|
|
|
|
|
"@@ -0,0 +1,3 @@",
|
|
|
|
|
"+line one",
|
|
|
|
|
"+line two",
|
|
|
|
|
"+line three",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 1)
|
|
|
|
|
|
|
|
|
|
let file = result.files[0]
|
|
|
|
|
#expect(file.oldName == "/dev/null")
|
|
|
|
|
#expect(file.newName == "hello.txt")
|
|
|
|
|
#expect(file.hunks.count == 1)
|
|
|
|
|
|
|
|
|
|
let hunk = file.hunks[0]
|
|
|
|
|
#expect(hunk.lines.count == 4)
|
|
|
|
|
#expect(hunk.lines[0].type == .header)
|
|
|
|
|
#expect(hunk.lines[1].type == .addition)
|
|
|
|
|
#expect(hunk.lines[2].type == .addition)
|
|
|
|
|
#expect(hunk.lines[3].type == .addition)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseSingleFileDeletion() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/old.txt b/old.txt",
|
|
|
|
|
"deleted file mode 100644",
|
|
|
|
|
"--- a/old.txt",
|
|
|
|
|
"+++ /dev/null",
|
|
|
|
|
"@@ -1,2 +0,0 @@",
|
|
|
|
|
"-removed line one",
|
|
|
|
|
"-removed line two",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 1)
|
|
|
|
|
|
|
|
|
|
let file = result.files[0]
|
|
|
|
|
#expect(file.oldName == "old.txt")
|
|
|
|
|
#expect(file.newName == "/dev/null")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseSingleFileModification() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/file.txt b/file.txt",
|
|
|
|
|
"--- a/file.txt",
|
|
|
|
|
"+++ b/file.txt",
|
|
|
|
|
"@@ -1,3 +1,3 @@",
|
|
|
|
|
" context line",
|
|
|
|
|
"-old line",
|
|
|
|
|
"+new line",
|
|
|
|
|
" another context",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 1)
|
|
|
|
|
|
|
|
|
|
let hunk = result.files[0].hunks[0]
|
|
|
|
|
#expect(hunk.lines.count == 5)
|
|
|
|
|
|
|
|
|
|
let contextLine = hunk.lines[1]
|
|
|
|
|
#expect(contextLine.type == .context)
|
|
|
|
|
#expect(contextLine.oldLineNumber == 1)
|
|
|
|
|
#expect(contextLine.newLineNumber == 1)
|
|
|
|
|
|
|
|
|
|
let deletionLine = hunk.lines[2]
|
|
|
|
|
#expect(deletionLine.type == .deletion)
|
|
|
|
|
#expect(deletionLine.content == "old line")
|
|
|
|
|
#expect(deletionLine.oldLineNumber == 2)
|
|
|
|
|
#expect(deletionLine.newLineNumber == nil)
|
|
|
|
|
|
|
|
|
|
let additionLine = hunk.lines[3]
|
|
|
|
|
#expect(additionLine.type == .addition)
|
|
|
|
|
#expect(additionLine.content == "new line")
|
|
|
|
|
#expect(additionLine.oldLineNumber == nil)
|
|
|
|
|
#expect(additionLine.newLineNumber == 2)
|
|
|
|
|
|
|
|
|
|
let trailingContext = hunk.lines[4]
|
|
|
|
|
#expect(trailingContext.type == .context)
|
|
|
|
|
#expect(trailingContext.oldLineNumber == 3)
|
|
|
|
|
#expect(trailingContext.newLineNumber == 3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Multiple files
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseMultipleFiles() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/a.txt b/a.txt",
|
|
|
|
|
"--- a/a.txt",
|
|
|
|
|
"+++ b/a.txt",
|
|
|
|
|
"@@ -1 +1 @@",
|
|
|
|
|
"-old a",
|
|
|
|
|
"+new a",
|
|
|
|
|
"diff --git a/b.txt b/b.txt",
|
|
|
|
|
"--- a/b.txt",
|
|
|
|
|
"+++ b/b.txt",
|
|
|
|
|
"@@ -1 +1 @@",
|
|
|
|
|
"-old b",
|
|
|
|
|
"+new b",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 2)
|
|
|
|
|
#expect(result.files[0].newName == "a.txt")
|
|
|
|
|
#expect(result.files[1].newName == "b.txt")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Multiple hunks
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseMultipleHunks() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/file.txt b/file.txt",
|
|
|
|
|
"--- a/file.txt",
|
|
|
|
|
"+++ b/file.txt",
|
|
|
|
|
"@@ -1,3 +1,3 @@",
|
|
|
|
|
" context",
|
|
|
|
|
"-old first",
|
|
|
|
|
"+new first",
|
|
|
|
|
" context",
|
|
|
|
|
"@@ -10,3 +10,3 @@",
|
|
|
|
|
" context",
|
|
|
|
|
"-old second",
|
|
|
|
|
"+new second",
|
|
|
|
|
" context",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files[0].hunks.count == 2)
|
|
|
|
|
|
|
|
|
|
let hunk2 = result.files[0].hunks[1]
|
|
|
|
|
let contextLine = hunk2.lines[1]
|
|
|
|
|
#expect(contextLine.oldLineNumber == 10)
|
|
|
|
|
#expect(contextLine.newLineNumber == 10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Line number tracking
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func lineNumbersTrackCorrectlyWithMultipleAdditions() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/file.txt b/file.txt",
|
|
|
|
|
"--- a/file.txt",
|
|
|
|
|
"+++ b/file.txt",
|
|
|
|
|
"@@ -5,4 +5,6 @@",
|
|
|
|
|
" context",
|
|
|
|
|
"+added one",
|
|
|
|
|
"+added two",
|
|
|
|
|
" old context",
|
|
|
|
|
"-removed",
|
|
|
|
|
"+replaced",
|
|
|
|
|
" trailing",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
let lines = result.files[0].hunks[0].lines
|
|
|
|
|
|
|
|
|
|
let ctx1 = lines[1]
|
|
|
|
|
#expect(ctx1.oldLineNumber == 5)
|
|
|
|
|
#expect(ctx1.newLineNumber == 5)
|
|
|
|
|
|
|
|
|
|
let add1 = lines[2]
|
|
|
|
|
#expect(add1.newLineNumber == 6)
|
|
|
|
|
#expect(add1.oldLineNumber == nil)
|
|
|
|
|
|
|
|
|
|
let add2 = lines[3]
|
|
|
|
|
#expect(add2.newLineNumber == 7)
|
|
|
|
|
|
|
|
|
|
let ctx2 = lines[4]
|
|
|
|
|
#expect(ctx2.oldLineNumber == 6)
|
|
|
|
|
#expect(ctx2.newLineNumber == 8)
|
|
|
|
|
|
|
|
|
|
let del = lines[5]
|
|
|
|
|
#expect(del.oldLineNumber == 7)
|
|
|
|
|
#expect(del.newLineNumber == nil)
|
|
|
|
|
|
|
|
|
|
let repl = lines[6]
|
|
|
|
|
#expect(repl.newLineNumber == 9)
|
|
|
|
|
#expect(repl.oldLineNumber == nil)
|
|
|
|
|
|
|
|
|
|
let trailing = lines[7]
|
|
|
|
|
#expect(trailing.oldLineNumber == 8)
|
|
|
|
|
#expect(trailing.newLineNumber == 10)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Hunk header parsing
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func hunkHeaderWithFunctionContext() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/main.swift b/main.swift",
|
|
|
|
|
"--- a/main.swift",
|
|
|
|
|
"+++ b/main.swift",
|
|
|
|
|
"@@ -42,6 +42,7 @@ func doSomething() {",
|
|
|
|
|
" context",
|
|
|
|
|
"+new line",
|
|
|
|
|
" context",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
let firstLine = result.files[0].hunks[0].lines[1]
|
|
|
|
|
#expect(firstLine.oldLineNumber == 42)
|
|
|
|
|
#expect(firstLine.newLineNumber == 42)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - File name extraction
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func fileNameExtractedFromMinusPlus() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/src/app.swift b/src/app.swift",
|
|
|
|
|
"--- a/src/app.swift",
|
|
|
|
|
"+++ b/src/app.swift",
|
|
|
|
|
"@@ -1 +1 @@",
|
|
|
|
|
"-old",
|
|
|
|
|
"+new",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files[0].oldName == "src/app.swift")
|
|
|
|
|
#expect(result.files[0].newName == "src/app.swift")
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func fileNameFallsBackToGitHeader() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/image.png b/image.png",
|
|
|
|
|
"Binary files differ",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 1)
|
|
|
|
|
#expect(result.files[0].oldName == "image.png")
|
|
|
|
|
#expect(result.files[0].newName == "image.png")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Commit diff (new file from Forgejo API)
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseCommitDiffNewFile() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/src/main.py b/src/main.py",
|
|
|
|
|
"new file mode 100644",
|
|
|
|
|
"index 0000000..67e9324",
|
|
|
|
|
"--- /dev/null",
|
|
|
|
|
"+++ b/src/main.py",
|
|
|
|
|
"@@ -0,0 +1 @@",
|
|
|
|
|
"+def main():\\n print(\"Main module\")",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 1)
|
|
|
|
|
|
|
|
|
|
let file = result.files[0]
|
|
|
|
|
#expect(file.oldName == "/dev/null")
|
|
|
|
|
#expect(file.newName == "src/main.py")
|
|
|
|
|
#expect(file.hunks.count == 1)
|
|
|
|
|
|
|
|
|
|
let hunk = file.hunks[0]
|
|
|
|
|
#expect(hunk.lines[0].type == .header)
|
|
|
|
|
#expect(hunk.lines[1].type == .addition)
|
|
|
|
|
#expect(hunk.lines[1].newLineNumber == 1)
|
|
|
|
|
#expect(hunk.lines[1].oldLineNumber == nil)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func parseCommitDiffMultipleFiles() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/hello.py b/hello.py",
|
|
|
|
|
"new file mode 100644",
|
|
|
|
|
"--- /dev/null",
|
|
|
|
|
"+++ b/hello.py",
|
|
|
|
|
"@@ -0,0 +1,2 @@",
|
|
|
|
|
"+print(\"hello\")",
|
|
|
|
|
"+print(\"world\")",
|
|
|
|
|
"diff --git a/README.md b/README.md",
|
|
|
|
|
"--- a/README.md",
|
|
|
|
|
"+++ b/README.md",
|
|
|
|
|
"@@ -1,2 +1,3 @@",
|
|
|
|
|
" # Project",
|
|
|
|
|
"+",
|
|
|
|
|
"+New section",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
#expect(result.files.count == 2)
|
|
|
|
|
|
|
|
|
|
#expect(result.files[0].newName == "hello.py")
|
|
|
|
|
#expect(result.files[0].oldName == "/dev/null")
|
2026-06-04 02:29:23 -07:00
|
|
|
#expect(result.files[0].hunks[0].lines.count(where: { $0.type == .addition }) == 2)
|
2026-02-28 11:10:51 -08:00
|
|
|
|
|
|
|
|
#expect(result.files[1].newName == "README.md")
|
|
|
|
|
#expect(result.files[1].oldName == "README.md")
|
|
|
|
|
let readmeHunk = result.files[1].hunks[0]
|
2026-06-04 02:29:23 -07:00
|
|
|
#expect(readmeHunk.lines.count(where: { $0.type == .context }) == 1)
|
|
|
|
|
#expect(readmeHunk.lines.count(where: { $0.type == .addition }) == 2)
|
2026-02-28 11:10:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - No newline at end of file sentinel
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func noNewlineAtEndOfFileSentinelIsSkipped() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/file.txt b/file.txt",
|
|
|
|
|
"--- a/file.txt",
|
|
|
|
|
"+++ b/file.txt",
|
|
|
|
|
"@@ -1,2 +1,2 @@",
|
|
|
|
|
" context",
|
|
|
|
|
"-old line",
|
|
|
|
|
"\\ No newline at end of file",
|
|
|
|
|
"+new line",
|
|
|
|
|
"\\ No newline at end of file",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
let hunk = result.files[0].hunks[0]
|
|
|
|
|
// Header + context + deletion + addition = 4 lines (sentinels skipped)
|
|
|
|
|
#expect(hunk.lines.count == 4)
|
|
|
|
|
#expect(hunk.lines[0].type == .header)
|
|
|
|
|
#expect(hunk.lines[1].type == .context)
|
|
|
|
|
#expect(hunk.lines[2].type == .deletion)
|
|
|
|
|
#expect(hunk.lines[3].type == .addition)
|
|
|
|
|
// diffPosition should be consecutive without gaps from the sentinel
|
|
|
|
|
#expect(hunk.lines[1].diffPosition == 1)
|
|
|
|
|
#expect(hunk.lines[2].diffPosition == 2)
|
|
|
|
|
#expect(hunk.lines[3].diffPosition == 3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - DiffLine identity
|
|
|
|
|
|
2026-06-15 03:06:27 -07:00
|
|
|
@Test func eachLineHasUniqueId() {
|
2026-02-28 11:10:51 -08:00
|
|
|
let diff = [
|
|
|
|
|
"diff --git a/f.txt b/f.txt",
|
|
|
|
|
"--- a/f.txt",
|
|
|
|
|
"+++ b/f.txt",
|
|
|
|
|
"@@ -1,2 +1,2 @@",
|
|
|
|
|
"-a",
|
|
|
|
|
"+b",
|
|
|
|
|
].joined(separator: "\n")
|
|
|
|
|
|
|
|
|
|
let result = DiffParser.parse(diff)
|
|
|
|
|
let ids = result.files[0].hunks[0].lines.map(\.id)
|
|
|
|
|
let uniqueIds = Set(ids)
|
|
|
|
|
#expect(ids.count == uniqueIds.count)
|
|
|
|
|
}
|
|
|
|
|
}
|