master
  1package gitdiff
  2
  3import (
  4	"bytes"
  5	"encoding/binary"
  6	"encoding/json"
  7	"io"
  8	"os"
  9	"reflect"
 10	"testing"
 11)
 12
 13func TestLineOperations(t *testing.T) {
 14	const content = "the first line\nthe second line\nthe third line\n"
 15
 16	t.Run("read", func(t *testing.T) {
 17		p := newTestParser(content, false)
 18
 19		for i, expected := range []string{
 20			"the first line\n",
 21			"the second line\n",
 22			"the third line\n",
 23		} {
 24			if err := p.Next(); err != nil {
 25				t.Fatalf("error advancing parser after line %d: %v", i, err)
 26			}
 27			if p.lineno != int64(i+1) {
 28				t.Fatalf("incorrect line number: expected %d, actual: %d", i+1, p.lineno)
 29			}
 30
 31			line := p.Line(0)
 32			if line != expected {
 33				t.Fatalf("incorrect line %d: expected %q, was %q", i+1, expected, line)
 34			}
 35		}
 36
 37		// reading after the last line should return EOF
 38		if err := p.Next(); err != io.EOF {
 39			t.Fatalf("expected EOF after end, but got: %v", err)
 40		}
 41		if p.lineno != 4 {
 42			t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno)
 43		}
 44
 45		// reading again returns EOF again and does not advance the line
 46		if err := p.Next(); err != io.EOF {
 47			t.Fatalf("expected EOF after end, but got: %v", err)
 48		}
 49		if p.lineno != 4 {
 50			t.Fatalf("incorrect line number: expected %d, actual: %d", 4, p.lineno)
 51		}
 52	})
 53
 54	t.Run("peek", func(t *testing.T) {
 55		p := newTestParser(content, false)
 56		if err := p.Next(); err != nil {
 57			t.Fatalf("error advancing parser: %v", err)
 58		}
 59
 60		line := p.Line(1)
 61		if line != "the second line\n" {
 62			t.Fatalf("incorrect peek line: %s", line)
 63		}
 64
 65		if err := p.Next(); err != nil {
 66			t.Fatalf("error advancing parser after peek: %v", err)
 67		}
 68
 69		line = p.Line(0)
 70		if line != "the second line\n" {
 71			t.Fatalf("incorrect read line: %s", line)
 72		}
 73	})
 74
 75	t.Run("emptyInput", func(t *testing.T) {
 76		p := newTestParser("", false)
 77		if err := p.Next(); err != io.EOF {
 78			t.Fatalf("expected EOF on first Next(), but got: %v", err)
 79		}
 80	})
 81}
 82
 83func TestParserInvariant_Advancement(t *testing.T) {
 84	tests := map[string]struct {
 85		Input   string
 86		Parse   func(p *parser) error
 87		EndLine string
 88	}{
 89		"ParseGitFileHeader": {
 90			Input: `diff --git a/dir/file.txt b/dir/file.txt
 91index 9540595..30e6333 100644
 92--- a/dir/file.txt
 93+++ b/dir/file.txt
 94@@ -1,2 +1,3 @@
 95context line
 96`,
 97			Parse: func(p *parser) error {
 98				_, err := p.ParseGitFileHeader()
 99				return err
100			},
101			EndLine: "@@ -1,2 +1,3 @@\n",
102		},
103		"ParseTraditionalFileHeader": {
104			Input: `--- dir/file.txt
105+++ dir/file.txt
106@@ -1,2 +1,3 @@
107context line
108`,
109			Parse: func(p *parser) error {
110				_, err := p.ParseTraditionalFileHeader()
111				return err
112			},
113			EndLine: "@@ -1,2 +1,3 @@\n",
114		},
115		"ParseTextFragmentHeader": {
116			Input: `@@ -1,2 +1,3 @@
117context line
118`,
119			Parse: func(p *parser) error {
120				_, err := p.ParseTextFragmentHeader()
121				return err
122			},
123			EndLine: "context line\n",
124		},
125		"ParseTextChunk": {
126			Input: ` context line
127-old line
128+new line
129 context line
130@@ -1 +1 @@
131`,
132			Parse: func(p *parser) error {
133				return p.ParseTextChunk(&TextFragment{OldLines: 3, NewLines: 3})
134			},
135			EndLine: "@@ -1 +1 @@\n",
136		},
137		"ParseTextFragments": {
138			Input: `@@ -1,2 +1,2 @@
139 context line
140-old line
141+new line
142@@ -1,2 +1,2 @@
143-old line
144+new line
145 context line
146diff --git a/file.txt b/file.txt
147`,
148			Parse: func(p *parser) error {
149				_, err := p.ParseTextFragments(&File{})
150				return err
151			},
152			EndLine: "diff --git a/file.txt b/file.txt\n",
153		},
154		"ParseNextFileHeader": {
155			Input: `not a header
156diff --git a/file.txt b/file.txt
157--- a/file.txt
158+++ b/file.txt
159@@ -1,2 +1,2 @@
160`,
161			Parse: func(p *parser) error {
162				_, _, err := p.ParseNextFileHeader()
163				return err
164			},
165			EndLine: "@@ -1,2 +1,2 @@\n",
166		},
167		"ParseBinaryMarker": {
168			Input: `Binary files differ
169diff --git a/file.txt b/file.txt
170`,
171			Parse: func(p *parser) error {
172				_, _, err := p.ParseBinaryMarker()
173				return err
174			},
175			EndLine: "diff --git a/file.txt b/file.txt\n",
176		},
177		"ParseBinaryFragmentHeader": {
178			Input: `literal 0
179HcmV?d00001
180`,
181			Parse: func(p *parser) error {
182				_, err := p.ParseBinaryFragmentHeader()
183				return err
184			},
185			EndLine: "HcmV?d00001\n",
186		},
187		"ParseBinaryChunk": {
188			Input: "TcmZQzU|?i`" + `U?w2V48*Je09XJG
189
190literal 0
191`,
192			Parse: func(p *parser) error {
193				return p.ParseBinaryChunk(&BinaryFragment{Size: 20})
194			},
195			EndLine: "literal 0\n",
196		},
197		"ParseBinaryFragments": {
198			Input: `GIT binary patch
199literal 40
200gcmZQzU|?i` + "`" + `U?w2V48*KJ%mKu_Kr9NxN<eH500b)lkN^Mx
201
202literal 0
203HcmV?d00001
204
205diff --git a/file.txt b/file.txt
206`,
207			Parse: func(p *parser) error {
208				_, err := p.ParseBinaryFragments(&File{})
209				return err
210			},
211			EndLine: "diff --git a/file.txt b/file.txt\n",
212		},
213	}
214
215	for name, test := range tests {
216		t.Run(name, func(t *testing.T) {
217			p := newTestParser(test.Input, true)
218
219			if err := test.Parse(p); err != nil {
220				t.Fatalf("unexpected error while parsing: %v", err)
221			}
222
223			if test.EndLine != p.Line(0) {
224				t.Errorf("incorrect position after parsing\nexpected: %q\n  actual: %q", test.EndLine, p.Line(0))
225			}
226		})
227	}
228}
229
230func TestParseNextFileHeader(t *testing.T) {
231	tests := map[string]struct {
232		Input    string
233		Output   *File
234		Preamble string
235		Err      bool
236	}{
237		"gitHeader": {
238			Input: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
239Author:	Morton Haypenny <mhaypenny@example.com>
240Date:	Tue Apr 2 22:30:00 2019 -0700
241
242    This is a sample commit message.
243
244diff --git a/file.txt b/file.txt
245index cc34da1..1acbae5 100644
246--- a/file.txt
247+++ b/file.txt
248@@ -1,3 +1,4 @@
249`,
250			Output: &File{
251				OldName:      "file.txt",
252				NewName:      "file.txt",
253				OldMode:      os.FileMode(0100644),
254				OldOIDPrefix: "cc34da1",
255				NewOIDPrefix: "1acbae5",
256			},
257			Preamble: `commit 1acbae563cd6ef5750a82ee64e116c6eb065cb94
258Author:	Morton Haypenny <mhaypenny@example.com>
259Date:	Tue Apr 2 22:30:00 2019 -0700
260
261    This is a sample commit message.
262
263`,
264		},
265		"traditionalHeader": {
266			Input: `
267--- file.txt	2019-04-01 22:58:14.833597918 -0700
268+++ file.txt	2019-04-01 22:58:14.833597918 -0700
269@@ -1,3 +1,4 @@
270`,
271			Output: &File{
272				OldName: "file.txt",
273				NewName: "file.txt",
274			},
275			Preamble: "\n",
276		},
277		"noHeaders": {
278			Input: `
279this is a line
280this is another line
281--- could this be a header?
282nope, it's just some dashes
283`,
284			Output: nil,
285			Preamble: `
286this is a line
287this is another line
288--- could this be a header?
289nope, it's just some dashes
290`,
291		},
292		"detatchedFragmentLike": {
293			Input: `
294a wild fragment appears?
295@@ -1,3 +1,4 ~1,5 @@
296`,
297			Output: nil,
298			Preamble: `
299a wild fragment appears?
300@@ -1,3 +1,4 ~1,5 @@
301`,
302		},
303		"detatchedFragment": {
304			Input: `
305a wild fragment appears?
306@@ -1,3 +1,4 @@
307`,
308			Err: true,
309		},
310	}
311
312	for name, test := range tests {
313		t.Run(name, func(t *testing.T) {
314			p := newTestParser(test.Input, true)
315
316			f, pre, err := p.ParseNextFileHeader()
317			if test.Err {
318				if err == nil || err == io.EOF {
319					t.Fatalf("expected error parsing next file header, but got %v", err)
320				}
321				return
322			}
323			if err != nil {
324				t.Fatalf("unexpected error parsing next file header: %v", err)
325			}
326
327			if test.Preamble != pre {
328				t.Errorf("incorrect preamble\nexpected: %q\n  actual: %q", test.Preamble, pre)
329			}
330			if !reflect.DeepEqual(test.Output, f) {
331				t.Errorf("incorrect file\nexpected: %+v\n  actual: %+v", test.Output, f)
332			}
333		})
334	}
335}
336
337func TestParse(t *testing.T) {
338	textFragments := []*TextFragment{
339		{
340			OldPosition: 3,
341			OldLines:    6,
342			NewPosition: 3,
343			NewLines:    8,
344			Comment:     "fragment 1",
345			Lines: []Line{
346				{OpContext, "context line\n"},
347				{OpDelete, "old line 1\n"},
348				{OpDelete, "old line 2\n"},
349				{OpContext, "context line\n"},
350				{OpAdd, "new line 1\n"},
351				{OpAdd, "new line 2\n"},
352				{OpAdd, "new line 3\n"},
353				{OpContext, "context line\n"},
354				{OpDelete, "old line 3\n"},
355				{OpAdd, "new line 4\n"},
356				{OpAdd, "new line 5\n"},
357			},
358			LinesAdded:     5,
359			LinesDeleted:   3,
360			LeadingContext: 1,
361		},
362		{
363			OldPosition: 31,
364			OldLines:    2,
365			NewPosition: 33,
366			NewLines:    2,
367			Comment:     "fragment 2",
368			Lines: []Line{
369				{OpContext, "context line\n"},
370				{OpDelete, "old line 4\n"},
371				{OpAdd, "new line 6\n"},
372			},
373			LinesAdded:     1,
374			LinesDeleted:   1,
375			LeadingContext: 1,
376		},
377	}
378
379	textPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
380Author: Morton Haypenny <mhaypenny@example.com>
381Date:   Tue Apr 2 22:55:40 2019 -0700
382
383    A file with multiple fragments.
384
385    The content is arbitrary.
386
387`
388
389	binaryPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
390Author: Morton Haypenny <mhaypenny@example.com>
391Date:   Tue Apr 2 22:55:40 2019 -0700
392
393    A binary file with the first 10 fibonacci numbers.
394
395`
396	tests := map[string]struct {
397		InputFile string
398		Output    []*File
399		Preamble  string
400		Err       bool
401	}{
402		"oneFile": {
403			InputFile: "testdata/one_file.patch",
404			Output: []*File{
405				{
406					OldName:       "dir/file1.txt",
407					NewName:       "dir/file1.txt",
408					OldMode:       os.FileMode(0100644),
409					OldOIDPrefix:  "ebe9fa54",
410					NewOIDPrefix:  "fe103e1d",
411					TextFragments: textFragments,
412				},
413			},
414			Preamble: textPreamble,
415		},
416		"twoFiles": {
417			InputFile: "testdata/two_files.patch",
418			Output: []*File{
419				{
420					OldName:       "dir/file1.txt",
421					NewName:       "dir/file1.txt",
422					OldMode:       os.FileMode(0100644),
423					OldOIDPrefix:  "ebe9fa54",
424					NewOIDPrefix:  "fe103e1d",
425					TextFragments: textFragments,
426				},
427				{
428					OldName:       "dir/file2.txt",
429					NewName:       "dir/file2.txt",
430					OldMode:       os.FileMode(0100644),
431					OldOIDPrefix:  "417ebc70",
432					NewOIDPrefix:  "67514b7f",
433					TextFragments: textFragments,
434				},
435			},
436			Preamble: textPreamble,
437		},
438		"noFiles": {
439			InputFile: "testdata/no_files.patch",
440			Output:    nil,
441			Preamble:  textPreamble,
442		},
443		"newBinaryFile": {
444			InputFile: "testdata/new_binary_file.patch",
445			Output: []*File{
446				{
447					OldName:      "",
448					NewName:      "dir/ten.bin",
449					NewMode:      os.FileMode(0100644),
450					OldOIDPrefix: "0000000000000000000000000000000000000000",
451					NewOIDPrefix: "77b068ba48c356156944ea714740d0d5ca07bfec",
452					IsNew:        true,
453					IsBinary:     true,
454					BinaryFragment: &BinaryFragment{
455						Method: BinaryPatchLiteral,
456						Size:   40,
457						Data:   fib(10, binary.BigEndian),
458					},
459					ReverseBinaryFragment: &BinaryFragment{
460						Method: BinaryPatchLiteral,
461						Size:   0,
462						Data:   []byte{},
463					},
464				},
465			},
466			Preamble: binaryPreamble,
467		},
468	}
469
470	for name, test := range tests {
471		t.Run(name, func(t *testing.T) {
472			f, err := os.Open(test.InputFile)
473			if err != nil {
474				t.Fatalf("unexpected error opening input file: %v", err)
475			}
476
477			files, pre, err := Parse(f)
478			if test.Err {
479				if err == nil || err == io.EOF {
480					t.Fatalf("expected error parsing patch, but got %v", err)
481				}
482				return
483			}
484			if err != nil {
485				t.Fatalf("unexpected error parsing patch: %v", err)
486			}
487
488			if len(test.Output) != len(files) {
489				t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files))
490			}
491			if test.Preamble != pre {
492				t.Errorf("incorrect preamble\nexpected: %q\n  actual: %q", test.Preamble, pre)
493			}
494			for i := range test.Output {
495				if !reflect.DeepEqual(test.Output[i], files[i]) {
496					exp, _ := json.MarshalIndent(test.Output[i], "", "  ")
497					act, _ := json.MarshalIndent(files[i], "", "  ")
498					t.Errorf("incorrect file at position %d\nexpected: %s\n  actual: %s", i, exp, act)
499				}
500			}
501		})
502	}
503}
504
505func newTestParser(input string, init bool) *parser {
506	p := newParser(bytes.NewBufferString(input))
507	if init {
508		_ = p.Next()
509	}
510	return p
511}