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: "",
286 },
287 "detatchedFragmentLike": {
288 Input: `
289a wild fragment appears?
290@@ -1,3 +1,4 ~1,5 @@
291`,
292 Output: nil,
293 },
294 "detatchedFragment": {
295 Input: `
296a wild fragment appears?
297@@ -1,3 +1,4 @@
298`,
299 Err: true,
300 },
301 }
302
303 for name, test := range tests {
304 t.Run(name, func(t *testing.T) {
305 p := newTestParser(test.Input, true)
306
307 f, pre, err := p.ParseNextFileHeader()
308 if test.Err {
309 if err == nil || err == io.EOF {
310 t.Fatalf("expected error parsing next file header, but got %v", err)
311 }
312 return
313 }
314 if err != nil {
315 t.Fatalf("unexpected error parsing next file header: %v", err)
316 }
317
318 if test.Preamble != pre {
319 t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
320 }
321 if !reflect.DeepEqual(test.Output, f) {
322 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f)
323 }
324 })
325 }
326}
327
328func TestParse(t *testing.T) {
329 textFragments := []*TextFragment{
330 {
331 OldPosition: 3,
332 OldLines: 6,
333 NewPosition: 3,
334 NewLines: 8,
335 Comment: "fragment 1",
336 Lines: []Line{
337 {OpContext, "context line\n"},
338 {OpDelete, "old line 1\n"},
339 {OpDelete, "old line 2\n"},
340 {OpContext, "context line\n"},
341 {OpAdd, "new line 1\n"},
342 {OpAdd, "new line 2\n"},
343 {OpAdd, "new line 3\n"},
344 {OpContext, "context line\n"},
345 {OpDelete, "old line 3\n"},
346 {OpAdd, "new line 4\n"},
347 {OpAdd, "new line 5\n"},
348 },
349 LinesAdded: 5,
350 LinesDeleted: 3,
351 LeadingContext: 1,
352 },
353 {
354 OldPosition: 31,
355 OldLines: 2,
356 NewPosition: 33,
357 NewLines: 2,
358 Comment: "fragment 2",
359 Lines: []Line{
360 {OpContext, "context line\n"},
361 {OpDelete, "old line 4\n"},
362 {OpAdd, "new line 6\n"},
363 },
364 LinesAdded: 1,
365 LinesDeleted: 1,
366 LeadingContext: 1,
367 },
368 }
369
370 textPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
371Author: Morton Haypenny <mhaypenny@example.com>
372Date: Tue Apr 2 22:55:40 2019 -0700
373
374 A file with multiple fragments.
375
376 The content is arbitrary.
377
378`
379
380 binaryPreamble := `commit 5d9790fec7d95aa223f3d20936340bf55ff3dcbe
381Author: Morton Haypenny <mhaypenny@example.com>
382Date: Tue Apr 2 22:55:40 2019 -0700
383
384 A binary file with the first 10 fibonacci numbers.
385
386`
387 tests := map[string]struct {
388 InputFile string
389 Output []*File
390 Preamble string
391 Err bool
392 }{
393 "oneFile": {
394 InputFile: "testdata/one_file.patch",
395 Output: []*File{
396 {
397 OldName: "dir/file1.txt",
398 NewName: "dir/file1.txt",
399 OldMode: os.FileMode(0100644),
400 OldOIDPrefix: "ebe9fa54",
401 NewOIDPrefix: "fe103e1d",
402 TextFragments: textFragments,
403 },
404 },
405 Preamble: textPreamble,
406 },
407 "twoFiles": {
408 InputFile: "testdata/two_files.patch",
409 Output: []*File{
410 {
411 OldName: "dir/file1.txt",
412 NewName: "dir/file1.txt",
413 OldMode: os.FileMode(0100644),
414 OldOIDPrefix: "ebe9fa54",
415 NewOIDPrefix: "fe103e1d",
416 TextFragments: textFragments,
417 },
418 {
419 OldName: "dir/file2.txt",
420 NewName: "dir/file2.txt",
421 OldMode: os.FileMode(0100644),
422 OldOIDPrefix: "417ebc70",
423 NewOIDPrefix: "67514b7f",
424 TextFragments: textFragments,
425 },
426 },
427 Preamble: textPreamble,
428 },
429 "newBinaryFile": {
430 InputFile: "testdata/new_binary_file.patch",
431 Output: []*File{
432 {
433 OldName: "",
434 NewName: "dir/ten.bin",
435 NewMode: os.FileMode(0100644),
436 OldOIDPrefix: "0000000000000000000000000000000000000000",
437 NewOIDPrefix: "77b068ba48c356156944ea714740d0d5ca07bfec",
438 IsNew: true,
439 IsBinary: true,
440 BinaryFragment: &BinaryFragment{
441 Method: BinaryPatchLiteral,
442 Size: 40,
443 Data: fib(10, binary.BigEndian),
444 },
445 ReverseBinaryFragment: &BinaryFragment{
446 Method: BinaryPatchLiteral,
447 Size: 0,
448 Data: []byte{},
449 },
450 },
451 },
452 Preamble: binaryPreamble,
453 },
454 }
455
456 for name, test := range tests {
457 t.Run(name, func(t *testing.T) {
458 f, err := os.Open(test.InputFile)
459 if err != nil {
460 t.Fatalf("unexpected error opening input file: %v", err)
461 }
462
463 files, pre, err := Parse(f)
464 if test.Err {
465 if err == nil || err == io.EOF {
466 t.Fatalf("expected error parsing patch, but got %v", err)
467 }
468 return
469 }
470 if err != nil {
471 t.Fatalf("unexpected error parsing patch: %v", err)
472 }
473
474 if len(test.Output) != len(files) {
475 t.Fatalf("incorrect number of parsed files: expected %d, actual %d", len(test.Output), len(files))
476 }
477 if test.Preamble != pre {
478 t.Errorf("incorrect preamble\nexpected: %q\n actual: %q", test.Preamble, pre)
479 }
480 for i := range test.Output {
481 if !reflect.DeepEqual(test.Output[i], files[i]) {
482 exp, _ := json.MarshalIndent(test.Output[i], "", " ")
483 act, _ := json.MarshalIndent(files[i], "", " ")
484 t.Errorf("incorrect file at position %d\nexpected: %s\n actual: %s", i, exp, act)
485 }
486 }
487 })
488 }
489}
490
491func newTestParser(input string, init bool) *parser {
492 p := newParser(bytes.NewBufferString(input))
493 if init {
494 _ = p.Next()
495 }
496 return p
497}