fork of go-gitdiff with jj support
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}