ref:ea217cc7e3afc70869df2a18fb6159ad409ee29c

test: add comprehensive tests verifying all algorithmic edge cases and fault isolation

SHA: ea217cc7e3afc70869df2a18fb6159ad409ee29c
Author: t <t@t.com>
Date: 2026-09-01 04:20
Parents: a778ca2
4 files changed +212 -0
Type
lib/ex_git_objectstore/diff.ex +20 −0
@@ -145,6 +145,17 @@
end
defp build_file_diff(repo, change, opts) do
try do
if hook = Keyword.get(opts, :file_diff_hook), do: hook.(change)
do_build_file_diff(repo, change, opts)
rescue
_ -> fallback_file_diff(change)
catch
_, _ -> fallback_file_diff(change)
end
end
defp do_build_file_diff(repo, change, opts) do
case diff_blobs(repo, change.old_sha, change.new_sha, opts) do
{:ok, :binary, sizes} ->
%{
@@ -159,6 +170,15 @@
{:ok, hunks} ->
%{path: change.path, status: change.status, hunks: hunks}
end
end
defp fallback_file_diff(change) do
%{
path: change.path,
status: change.status,
hunks: [],
binary: true
}
end
@doc """
test/ex_git_objectstore/diff/diff_test.exs +149 −0
@@ -105,6 +105,80 @@
{:ok, changes} = Diff.diff_trees(repo, root1_sha, root2_sha)
assert [%{path: "src/main.ex", status: :modified}] = changes
end
test "file replaced by directory with neighboring ascii entries" do
repo = RepoHelper.memory_repo()
blob_foo = Blob.from_content("file content\n")
{:ok, blob_foo_sha} = Object.write(repo, blob_foo)
blob_bar = Blob.from_content("bar content\n")
{:ok, blob_bar_sha} = Object.write(repo, blob_bar)
blob_foobar = Blob.from_content("foo_bar content\n")
{:ok, blob_foobar_sha} = Object.write(repo, blob_foobar)
blob_footxt = Blob.from_content("foo.txt content\n")
{:ok, blob_footxt_sha} = Object.write(repo, blob_footxt)
# Old tree has "foo" (file), "foo.txt", "foo_bar"
tree1 =
Tree.new([
%{mode: "100644", name: "foo", sha: blob_foo_sha},
%{mode: "100644", name: "foo.txt", sha: blob_footxt_sha},
%{mode: "100644", name: "foo_bar", sha: blob_foobar_sha}
])
{:ok, tree1_sha} = Object.write(repo, tree1)
# Subtree for directory "foo" containing "bar.txt"
sub_foo = Tree.new([%{mode: "100644", name: "bar.txt", sha: blob_bar_sha}])
{:ok, sub_foo_sha} = Object.write(repo, sub_foo)
# New tree has "foo" (directory), "foo.txt", "foo_bar"
tree2 =
Tree.new([
%{mode: "40000", name: "foo", sha: sub_foo_sha},
%{mode: "100644", name: "foo.txt", sha: blob_footxt_sha},
%{mode: "100644", name: "foo_bar", sha: blob_foobar_sha}
])
{:ok, tree2_sha} = Object.write(repo, tree2)
{:ok, changes} = Diff.diff_trees(repo, tree1_sha, tree2_sha)
paths_and_statuses = Enum.map(changes, fn c -> {c.path, c.status} end)
assert {"foo", :deleted} in paths_and_statuses
assert {"foo/bar.txt", :added} in paths_and_statuses
# Unchanged neighbor files must NOT be in diff
refute {"foo.txt", :modified} in paths_and_statuses
refute {"foo_bar", :modified} in paths_and_statuses
end
test "directory replaced by file" do
repo = RepoHelper.memory_repo()
blob_bar = Blob.from_content("bar content\n")
{:ok, blob_bar_sha} = Object.write(repo, blob_bar)
blob_foo = Blob.from_content("new file content\n")
{:ok, blob_foo_sha} = Object.write(repo, blob_foo)
sub_foo = Tree.new([%{mode: "100644", name: "bar.txt", sha: blob_bar_sha}])
{:ok, sub_foo_sha} = Object.write(repo, sub_foo)
tree1 = Tree.new([%{mode: "40000", name: "foo", sha: sub_foo_sha}])
{:ok, tree1_sha} = Object.write(repo, tree1)
tree2 = Tree.new([%{mode: "100644", name: "foo", sha: blob_foo_sha}])
{:ok, tree2_sha} = Object.write(repo, tree2)
{:ok, changes} = Diff.diff_trees(repo, tree1_sha, tree2_sha)
paths_and_statuses = Enum.map(changes, fn c -> {c.path, c.status} end)
assert {"foo/bar.txt", :deleted} in paths_and_statuses
assert {"foo", :added} in paths_and_statuses
end
end
describe "diff_blobs" do
@@ -313,6 +387,81 @@
{:ok, diffs} = Diff.diff_commits(repo, c1_sha, c2_sha)
assert [%{path: "file.txt", status: :modified, hunks: hunks}] = diffs
assert hunks != []
end
test "diff between commits with identical tree returns empty list" do
repo = RepoHelper.memory_repo()
ExGitObjectstore.init(repo)
blob1 = Blob.from_content("hello\n")
{:ok, b1_sha} = Object.write(repo, blob1)
tree1 = Tree.new([%{mode: "100644", name: "file.txt", sha: b1_sha}])
{:ok, t1_sha} = Object.write(repo, tree1)
c1 = %Commit{
tree: t1_sha,
parents: [],
author: "A <a@a.com> 1000000000 +0000",
committer: "A <a@a.com> 1000000000 +0000",
message: "first\n"
}
{:ok, c1_sha} = Object.write(repo, c1)
c2 = %Commit{
tree: t1_sha,
parents: [c1_sha],
author: "A <a@a.com> 1000000001 +0000",
committer: "A <a@a.com> 1000000001 +0000",
message: "empty commit\n"
}
{:ok, c2_sha} = Object.write(repo, c2)
assert {:ok, []} = Diff.diff_commits(repo, c1_sha, c2_sha)
end
test "diff_commits handles task crash or timeout gracefully without raising" do
repo = RepoHelper.memory_repo()
ExGitObjectstore.init(repo)
blob1 = Blob.from_content("hello\n")
{:ok, b1_sha} = Object.write(repo, blob1)
tree1 = Tree.new([%{mode: "100644", name: "file.txt", sha: b1_sha}])
{:ok, t1_sha} = Object.write(repo, tree1)
blob2 = Blob.from_content("world\n")
{:ok, b2_sha} = Object.write(repo, blob2)
tree2 = Tree.new([%{mode: "100644", name: "file.txt", sha: b2_sha}])
{:ok, t2_sha} = Object.write(repo, tree2)
c1 = %Commit{
tree: t1_sha,
parents: [],
author: "A <a@a.com> 1000000000 +0000",
committer: "A <a@a.com> 1000000000 +0000",
message: "first\n"
}
{:ok, c1_sha} = Object.write(repo, c1)
c2 = %Commit{
tree: t2_sha,
parents: [c1_sha],
author: "A <a@a.com> 1000000001 +0000",
committer: "A <a@a.com> 1000000001 +0000",
message: "second\n"
}
{:ok, c2_sha} = Object.write(repo, c2)
# Simulated task exit is isolated and returns safe stub
{:ok, diffs} =
Diff.diff_commits(repo, c1_sha, c2_sha,
file_diff_hook: fn _change -> exit(:simulated_crash) end
)
assert [%{path: "file.txt", status: :modified, hunks: [], binary: true}] = diffs
end
end
end
test/ex_git_objectstore/diff/myers_test.exs +16 −0
@@ -173,6 +173,22 @@
ins = Enum.filter(result, fn {op, _} -> op == :ins end)
assert ins != []
end
test "empty strings and identical text" do
assert Myers.diff_lines("", "") == [{:eq, ""}]
assert Myers.diff_lines("hello\nworld", "hello\nworld") == [{:eq, "hello"}, {:eq, "world"}]
end
test "line interning correctly preserves edits across repeated tokens" do
a = "a\nb\na\nb\na\n"
b = "a\nc\na\nc\na\n"
result = Myers.diff_lines(a, b)
lines_a = String.split(a, "\n", trim: false)
lines_b = String.split(b, "\n", trim: false)
assert apply_edits(lines_a, result) == lines_b
end
end
# Helper to verify edits transform a into b
test/ex_git_objectstore/graph/fallback_test.exs +27 −0
@@ -231,5 +231,32 @@
b = commit!(repo, t, [], 2)
assert {:ok, false} = Fallback.ancestor?(repo, a, b)
end
test "ancestor? terminates early on deep history without exceeding low max_walk" do
repo = init_repo()
t = tree!(repo)
chain = build_linear_chain(repo, t, 50)
tip = List.last(chain)
near_ancestor = Enum.at(chain, 48)
# Distance is 2 steps. With max_walk: 5, early-exit succeeds where full history walk fails.
assert {:ok, true} = Fallback.ancestor?(repo, near_ancestor, tip, max_walk: 5)
end
test "diamond merge ahead and behind calculation with branch cutoff" do
repo = init_repo()
t = tree!(repo)
root = commit!(repo, t, [], 1)
a = commit!(repo, t, [root], 2)
b = commit!(repo, t, [root], 3)
merge = commit!(repo, t, [a, b], 4)
# Merge is 2 ahead of root (a, b) and 0 behind
assert {:ok, %{ahead: 3, behind: 0}} = Fallback.ahead_behind(repo, root, merge)
# Merge is 1 ahead of a (commit b) + merge itself = 2 ahead, 0 behind
assert {:ok, %{ahead: 2, behind: 0}} = Fallback.ahead_behind(repo, a, merge)
# a vs b: 1 ahead, 1 behind
assert {:ok, %{ahead: 1, behind: 1}} = Fallback.ahead_behind(repo, a, b)
end
end
end