summaryrefslogtreecommitdiff
path: root/pkg/gitdiff/parser_test.go
diff options
context:
space:
mode:
authorAnton Medvedev <anton@medv.io>2025-11-30 12:46:34 +0100
committerAnton Medvedev <anton@medv.io>2025-11-30 12:46:34 +0100
commitf6b0f38af648d028422a7494378b5dabdc90573f (patch)
tree3c26cfc269c021300a2d1e4e02623dd440c20226 /pkg/gitdiff/parser_test.go
First commit
Diffstat (limited to 'pkg/gitdiff/parser_test.go')
-rw-r--r--pkg/gitdiff/parser_test.go511
1 files changed, 511 insertions, 0 deletions
diff --git a/pkg/gitdiff/parser_test.go b/pkg/gitdiff/parser_test.go
new file mode 100644
index 0000000..15a5d67
--- /dev/null
+++ b/pkg/gitdiff/parser_test.go
@@ -0,0 +1,511 @@
+package gitdiff
+
+import (
+ "bytes"
+ "encoding/binary"
+ "encoding/json"
+ "io"
+ "os"
+ "reflect"
+ "testing"
+)
+
+func TestLineOperations(t *testing.T) {
+ const content = "the first line\nthe second line\nthe third line\n"
+
+ t.Run("read", func(t *testing.T) {
+ p := newTestParser(content, false)
+
+ for i, expected := range []string{
+ "the first line\n",
+ "the second line\n",
+ "the third line\n",
+ } {
+ if err := p.Next(); err != nil {
+ t.Fatalf("error advancing parser after line %d: %v", i, err)
+ }
+ if p.lineno != int64(i+1) {
+ t.Fatalf("incorrect line number: expected %d, actual: %d", i+1, p.lineno)
+ }
+
+ line := p.Line(0)
+ if line != expected {
+ t.Fatalf("incorrect line %d: expected %q, was %q", i+1, expected, line)
+ }
+ }
+
+ // reading after the last line should return EOF
+ if err := p.Next(); err != io.EOF {
+ t.Fatalf("expected EOF after end, but got: %v", err)
+ }
+ if p.lineno != 4 {
+ t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno)
+ }
+
+ // reading again returns EOF again and does not advance the line
+ if err := p.Next(); err != io.EOF {
+ t.Fatalf("expected EOF after end, but got: %v", err)
+ }
+ if p.lineno != 4 {
+ t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno)
+ }
+ })
+
+ t.Run("peek", func(t *testing.T) {
+ p := newTestParser(content, false)
+ if err := p.Next(); err != nil {
+ t.Fatalf("error advancing parser: %v", err)
+ }
+
+ line := p.Line(1)
+ if line != "the second line\n" {
+ t.Fatalf("incorrect peek line: %s", line)
+ }
+
+ if err := p.Next(); err != nil {
+ t.Fatalf("error advancing parser after peek: %v", err)
+ }
+
+ line = p.Line(0)
+ if line != "the second line\n" {
+ t.Fatalf("incorrect read line: %s", line)
+ }
+ })
+
+ t.Run("emptyInput", func(t *testing.T) {
+ p := newTestParser("", false)
+ if err := p.Next(); err != io.EOF {
+ t.Fatalf("expected EOF on first Next(), but got: %v", err)
+ }
+ })
+}
+
+func TestParserInvariant_Advancement(t *testing.T) {
+ tests := map[string]struct {
+ Input string
+ Parse func(p *parser) error
+ EndLine string
+ }{
+ "ParseGitFileHeader": {
+ Input: `diff --git a/dir/file.txt b/dir/file.txt
+index 9540595..30e6333 100644
+--- a/dir/file.txt
++++ b/dir/file.txt
+@@ -1,2 +1,3 @@
+context line
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseGitFileHeader()
+ return err
+ },
+ EndLine: "@@ -1,2 +1,3 @@\n",
+ },
+ "ParseTraditionalFileHeader": {
+ Input: `--- dir/file.txt
++++ dir/file.txt
+@@ -1,2 +1,3 @@
+context line
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseTraditionalFileHeader()
+ return err
+ },
+ EndLine: "@@ -1,2 +1,3 @@\n",
+ },
+ "ParseTextFragmentHeader": {
+ Input: `@@ -1,2 +1,3 @@
+context line
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseTextFragmentHeader()
+ return err
+ },
+ EndLine: "context line\n",
+ },
+ "ParseTextChunk": {
+ Input: ` context line
+-old line
++new line
+ context line
+@@ -1 +1 @@
+`,
+ Parse: func(p *parser) error {
+ return p.ParseTextChunk(&TextFragment{OldLines: 3, NewLines: 3})
+ },
+ EndLine: "@@ -1 +1 @@\n",
+ },
+ "ParseTextFragments": {
+ Input: `@@ -1,2 +1,2 @@
+ context line
+-old line
++new line
+@@ -1,2 +1,2 @@
+-old line
++new line
+ context line
+diff --git a/file.txt b/file.txt
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseTextFragments(&File{})
+ return err
+ },
+ EndLine: "diff --git a/file.txt b/file.txt\n",
+ },
+ "ParseNextFileHeader": {
+ Input: `not a header
+diff --git a/file.txt b/file.txt
+--- a/file.txt
++++ b/file.txt
+@@ -1,2 +1,2 @@
+`,
+ Parse: func(p *parser) error {
+ _, _, err := p.ParseNextFileHeader()
+ return err
+ },
+ EndLine: "@@ -1,2 +1,2 @@\n",
+ },
+ "ParseBinaryMarker": {
+ Input: `Binary files differ
+diff --git a/file.txt b/file.txt
+`,
+ Parse: func(p *parser) error {
+ _, _, err := p.ParseBinaryMarker()
+ return err
+ },
+ EndLine: "diff --git a/file.txt b/file.txt\n",
+ },
+ "ParseBinaryFragmentHeader": {
+ Input: `literal 0
+HcmV?d00001
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseBinaryFragmentHeader()
+ return err
+ },
+ EndLine: "HcmV?d00001\n",
+ },
+ "ParseBinaryChunk": {
+ Input: "TcmZQzU|?i`" + `U?w2V48*Je09XJG
+
+literal 0
+`,
+ Parse: func(p *parser) error {
+ return p.ParseBinaryChunk(&BinaryFragment{Size: 20})
+ },
+ EndLine: "literal 0\n",
+ },
+ "ParseBinaryFragments": {
+ Input: `GIT binary patch
+literal 40
+gcmZQzU|?i` + "`" + `U?w2V48*KJ%mKu_Kr9NxN<eH500b)lkN^Mx
+
+literal 0
+HcmV?d00001
+
+diff --git a/file.txt b/file.txt
+`,
+ Parse: func(p *parser) error {
+ _, err := p.ParseBinaryFragments(&File{})
+ return err
+ },
+ EndLine: "diff --git a/file.txt b/file.txt\n",
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ p := newTestParser(test.Input, true)
+
+ if err := test.Parse(p); err != nil {
+ t.Fatalf("unexpected error while parsing: %v", err)
+ }
+
+ if test.EndLine != p.Line(0) {
+ t.Errorf("incorrect position after parsing\nexpected: %q\n actual: %q", test.EndLine, p.Line(0))
+ }
+ })
+ }
+}
+
+func TestParseNextFileHeader(t *testing.T) {
+ tests := map[string]struct {
+ Input string
+ Output *File
+ Preamble string
+ Err bool
+ }{
+ "gitHeader": {
+ Input: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
+Author: Morton Haypenny <mhaypenny@example.com>
+Date: Tue Apr 2 22:30:00 2019 -0700
+
+ This is a sample commit message.
+
+diff --git a/file.txt b/file.txt
+index cc34da1..1acbae5 100644
+--- a/file.txt
++++ b/file.txt
+@@ -1,3 +1,4 @@
+`,
+ Output: &File{
+ OldName: "file.txt",
+ NewName: "file.txt",
+ OldMode: os.FileMode(0100644),
+ OldOIDPrefix: "cc34da1",
+ NewOIDPrefix: "1acbae5",
+ },
+ Preamble: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
+Author: Morton Haypenny <mhaypenny@example.com>
+Date: Tue Apr 2 22:30:00 2019 -0700
+
+ This is a sample commit message.
+
+`,
+ },
+ "traditionalHeader": {
+ Input: `
+--- file.txt 2019-04-01 22:58:14.833597918 -0700
++++ file.txt 2019-04-01 22:58:14.833597918 -0700
+@@ -1,3 +1,4 @@
+`,
+ Output: &File{
+ OldName: "file.txt",
+ NewName: "file.txt",
+ },
+ Preamble: "\n",
+ },
+ "noHeaders": {
+ Input: `
+this is a line
+this is another line
+--- could this be a header?
+nope, it's just some dashes
+`,
+ Output: nil,
+ Preamble: `
+this is a line
+this is another line
+--- could this be a header?
+nope, it's just some dashes
+`,
+ },
+ "detatchedFragmentLike": {
+ Input: `
+a wild fragment appears?
+@@ -1,3 +1,4 ~1,5 @@
+`,
+ Output: nil,
+ Preamble: `
+a wild fragment appears?
+@@ -1,3 +1,4 ~1,5 @@
+`,
+ },
+ "detatchedFragment": {
+ Input: `
+a wild fragment appears?
+@@ -1,3 +1,4 @@
+`,
+ Err: true,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ p := newTestParser(test.Input, true)
+
+ f, pre, err := p.ParseNextFileHeader()
+ if test.Err {
+ if err == nil || err == io.EOF {
+ t.Fatalf("expected error parsing next file header, but got %v", err)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error parsing next file header: %v", err)
+ }
+
+ if test.Preamble != pre {
+ t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
+ }
+ if !reflect.DeepEqual(test.Output, f) {
+ t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
+ }
+ })
+ }
+}
+
+func TestParse(t *testing.T) {
+ textFragments := []*TextFragment{
+ {
+ OldPosition: 3,
+ OldLines: 6,
+ NewPosition: 3,
+ NewLines: 8,
+ Comment: "fragment 1",
+ Lines: []Line{
+ {OpContext, "context line\n"},
+ {OpDelete, "old line 1\n"},
+ {OpDelete, "old line 2\n"},
+ {OpContext, "context line\n"},
+ {OpAdd, "new line 1\n"},
+ {OpAdd, "new line 2\n"},
+ {OpAdd, "new line 3\n"},
+ {OpContext, "context line\n"},
+ {OpDelete, "old line 3\n"},
+ {OpAdd, "new line 4\n"},
+ {OpAdd, "new line 5\n"},
+ },
+ LinesAdded: 5,
+ LinesDeleted: 3,
+ LeadingContext: 1,
+ },
+ {
+ OldPosition: 31,
+ OldLines: 2,
+ NewPosition: 33,
+ NewLines: 2,
+ Comment: "fragment 2",
+ Lines: []Line{
+ {OpContext, "context line\n"},
+ {OpDelete, "old line 4\n"},
+ {OpAdd, "new line 6\n"},
+ },
+ LinesAdded: 1,
+ LinesDeleted: 1,
+ LeadingContext: 1,
+ },
+ }
+
+ textPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
+Author: Morton Haypenny <mhaypenny@example.com>
+Date: Tue Apr 2 22:55:40 2019 -0700
+
+ A file with multiple fragments.
+
+ The content is arbitrary.
+
+`
+
+ binaryPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
+Author: Morton Haypenny <mhaypenny@example.com>
+Date: Tue Apr 2 22:55:40 2019 -0700
+
+ A binary file with the first 10 fibonacci numbers.
+
+`
+ tests := map[string]struct {
+ InputFile string
+ Output []*File
+ Preamble string
+ Err bool
+ }{
+ "oneFile": {
+ InputFile: "testdata/one_file.patch",
+ Output: []*File{
+ {
+ OldName: "dir/file1.txt",
+ NewName: "dir/file1.txt",
+ OldMode: os.FileMode(0100644),
+ OldOIDPrefix: "ebe9fa54",
+ NewOIDPrefix: "fe103e1d",
+ TextFragments: textFragments,
+ },
+ },
+ Preamble: textPreamble,
+ },
+ "twoFiles": {
+ InputFile: "testdata/two_files.patch",
+ Output: []*File{
+ {
+ OldName: "dir/file1.txt",
+ NewName: "dir/file1.txt",
+ OldMode: os.FileMode(0100644),
+ OldOIDPrefix: "ebe9fa54",
+ NewOIDPrefix: "fe103e1d",
+ TextFragments: textFragments,
+ },
+ {
+ OldName: "dir/file2.txt",
+ NewName: "dir/file2.txt",
+ OldMode: os.FileMode(0100644),
+ OldOIDPrefix: "417ebc70",
+ NewOIDPrefix: "67514b7f",
+ TextFragments: textFragments,
+ },
+ },
+ Preamble: textPreamble,
+ },
+ "noFiles": {
+ InputFile: "testdata/no_files.patch",
+ Output: nil,
+ Preamble: textPreamble,
+ },
+ "newBinaryFile": {
+ InputFile: "testdata/new_binary_file.patch",
+ Output: []*File{
+ {
+ OldName: "",
+ NewName: "dir/ten.bin",
+ NewMode: os.FileMode(0100644),
+ OldOIDPrefix: "0000000000000000000000000000000000000000",
+ NewOIDPrefix: "77b068ba48c356156944ea714740d0d5ca07bfec",
+ IsNew: true,
+ IsBinary: true,
+ BinaryFragment: &BinaryFragment{
+ Method: BinaryPatchLiteral,
+ Size: 40,
+ Data: fib(10, binary.BigEndian),
+ },
+ ReverseBinaryFragment: &BinaryFragment{
+ Method: BinaryPatchLiteral,
+ Size: 0,
+ Data: []byte{},
+ },
+ },
+ },
+ Preamble: binaryPreamble,
+ },
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ f, err := os.Open(test.InputFile)
+ if err != nil {
+ t.Fatalf("unexpected error opening input file: %v", err)
+ }
+
+ files, pre, err := Parse(f)
+ if test.Err {
+ if err == nil || err == io.EOF {
+ t.Fatalf("expected error parsing patch, but got %v", err)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error parsing patch: %v", err)
+ }
+
+ if len(test.Output) != len(files) {
+ t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files))
+ }
+ if test.Preamble != pre {
+ t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
+ }
+ for i := range test.Output {
+ if !reflect.DeepEqual(test.Output[i], files[i]) {
+ exp, _ := json.MarshalIndent(test.Output[i], "", " ")
+ act, _ := json.MarshalIndent(files[i], "", " ")
+ t.Errorf("incorrect file at position %d\nexpected: %s\n actual: %s", i, exp, act)
+ }
+ }
+ })
+ }
+}
+
+func newTestParser(input string, init bool) *parser {
+ p := newParser(bytes.NewBufferString(input))
+ if init {
+ _ = p.Next()
+ }
+ return p
+}