fork of go-gitdiff with jj support
at v0.7.4 12 kB view raw
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}