defmodule OpenAgents.DiffTest do
@moduledoc """
The diff parser reads output from `git diff-tree -p -M`, which means its
input is generated by something we do not control and describes a repository
we do not control. So these hold two things: that the ordinary shapes parse
exactly, and that the awkward ones -- truncation, binary files, headers this
code has never seen -- degrade instead of raising.
Line numbering gets the most attention. It is the whole reason for parsing a
diff rather than printing it, and it is the part with an off-by-one in every
direction available: the two sides stop agreeing at the first change, and
each kind of line advances one side, the other, or both.
"""
use ExUnit.Case, async: true
alias OpenAgents.Diff
describe "line numbering" do
test "the two sides diverge at the first change and stay diverged" do
[file] =
Diff.parse("""
diff --git a/a.ex b/a.ex
index 1111111..2222222 100644
--- a/a.ex
+++ b/a.ex
@@ -10,6 +10,7 @@ def run do
context one
-removed line
+added one
+added two
context two
""")
[hunk] = file.hunks
assert Enum.map(hunk.lines, &{&1.kind, &1.old_number, &1.new_number}) == [
{:context, 10, 10},
# A deletion exists only in the old file.
{:delete, 11, nil},
# Insertions exist only in the new one, and both take new numbers
# from where the deletion left the new side untouched.
{:insert, nil, 11},
{:insert, nil, 12},
# Context resumes on both, now two apart.
{:context, 12, 13}
]
end
test "a hunk header without counts means one line" do
[file] = Diff.parse("diff --git a/a b/a\n@@ -3 +3 @@\n-old\n+new\n")
[hunk] = file.hunks
assert {hunk.old_start, hunk.old_count} == {3, 1}
assert {hunk.new_start, hunk.new_count} == {3, 1}
end
test "each hunk numbers from its own start" do
[file] =
Diff.parse("""
diff --git a/a b/a
@@ -1,2 +1,2 @@
-a
+b
@@ -100,2 +100,2 @@ inside something
-c
+d
""")
assert [first, second] = file.hunks
assert Enum.map(first.lines, & &1.old_number) == [1, nil]
assert Enum.map(second.lines, & &1.old_number) == [100, nil]
assert second.heading == "inside something"
assert first.heading == nil
end
end
describe "file status" do
test "an added file" do
[file] = Diff.parse("diff --git a/n b/n\nnew file mode 100644\n@@ -0,0 +1 @@\n+hello\n")
assert file.status == :added
assert file.insertions == 1
assert file.deletions == 0
end
test "a deleted file" do
[file] = Diff.parse("diff --git a/g b/g\ndeleted file mode 100644\n@@ -1 +0,0 @@\n-bye\n")
assert file.status == :deleted
assert file.deletions == 1
end
test "a rename carries both paths" do
[file] =
Diff.parse("""
diff --git a/old/name.ex b/new/name.ex
similarity index 96%
rename from old/name.ex
rename to new/name.ex
""")
assert file.status == :renamed
assert file.old_path == "old/name.ex"
assert file.path == "new/name.ex"
end
test "a binary file is flagged rather than left looking unchanged" do
[file] =
Diff.parse("""
diff --git a/i.png b/i.png
index 4415be7..d9aaa46 100644
Binary files a/i.png and b/i.png differ
""")
assert file.binary?
assert file.hunks == []
end
test "a modification is the default, not a special case" do
[file] = Diff.parse("diff --git a/a b/a\n@@ -1 +1 @@\n-a\n+b\n")
assert file.status == :modified
end
end
describe "input we do not control" do
test "a diff truncated mid-hunk keeps the lines it did receive" do
# The upstream caps `Browse.diff/2`, so this is the normal state of a
# large commit rather than a corrupt one.
[file] =
Diff.parse("""
diff --git a/a b/a
@@ -1,900 +1,900 @@
one
two
""")
assert length(hd(file.hunks).lines) == 2
assert Enum.map(hd(file.hunks).lines, & &1.old_number) == [1, 2]
end
test "a line inside a hunk that is not a diff line is kept as meta" do
[file] =
Diff.parse("diff --git a/a b/a\n@@ -1 +1 @@\n-a\n\\ No newline at end of file\n+b\n")
kinds = hd(file.hunks).lines |> Enum.map(& &1.kind)
assert :meta in kinds
# Meta advances neither side: the numbering of the lines around it is
# unaffected by its presence.
assert Enum.map(hd(file.hunks).lines, &{&1.kind, &1.old_number, &1.new_number}) == [
{:delete, 1, nil},
{:meta, nil, nil},
{:insert, nil, 1}
]
end
test "an unrecognised @@ line does not raise" do
[file] = Diff.parse("diff --git a/a b/a\n@@ this is not a hunk header\n")
assert file.hunks == []
end
test "content before the first file header is not attributed to a file" do
files =
Diff.parse("commit abc123\nAuthor: someone\n\ndiff --git a/a b/a\n@@ -1 +1 @@\n+x\n")
assert [%{path: "a"}] = files
end
test "empty and nil input parse to nothing" do
assert Diff.parse("") == []
assert Diff.parse(nil) == []
assert Diff.parse("not a diff at all\njust some text\n") == []
end
test "a quoted path containing a space" do
[file] = Diff.parse(~s|diff --git "a/with space.ex" "b/with space.ex"\n|)
assert file.path == "with space.ex"
end
end
describe "totals" do
test "count the parsed lines, so they describe the diff actually shown" do
files =
Diff.parse("""
diff --git a/a b/a
@@ -1,2 +1,2 @@
-one
+uno
diff --git a/b b/b
new file mode 100644
@@ -0,0 +1,2 @@
+x
+y
""")
assert Diff.totals(files) == %{files: 2, insertions: 3, deletions: 1}
end
test "an empty diff totals to nothing" do
assert Diff.totals([]) == %{files: 0, insertions: 0, deletions: 0}
end
end
end