test/openagents/diff_test.exs

58e6347eeb72 · 6 KB

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