fork of go-gitdiff with jj support
at v0.4.0 16 kB view raw
1package gitdiff 2 3import ( 4 "io" 5 "os" 6 "reflect" 7 "testing" 8) 9 10func TestParseGitFileHeader(t *testing.T) { 11 tests := map[string]struct { 12 Input string 13 Output *File 14 Err bool 15 }{ 16 "fileContentChange": { 17 Input: `diff --git a/dir/file.txt b/dir/file.txt 18index 1c23fcc..40a1b33 100644 19--- a/dir/file.txt 20+++ b/dir/file.txt 21@@ -2,3 +4,5 @@ 22`, 23 Output: &File{ 24 OldName: "dir/file.txt", 25 NewName: "dir/file.txt", 26 OldMode: os.FileMode(0100644), 27 OldOIDPrefix: "1c23fcc", 28 NewOIDPrefix: "40a1b33", 29 }, 30 }, 31 "newFile": { 32 Input: `diff --git a/dir/file.txt b/dir/file.txt 33new file mode 100644 34index 0000000..f5711e4 35--- /dev/null 36+++ b/dir/file.txt 37`, 38 Output: &File{ 39 NewName: "dir/file.txt", 40 NewMode: os.FileMode(0100644), 41 OldOIDPrefix: "0000000", 42 NewOIDPrefix: "f5711e4", 43 IsNew: true, 44 }, 45 }, 46 "newEmptyFile": { 47 Input: `diff --git a/empty.txt b/empty.txt 48new file mode 100644 49index 0000000..e69de29 50`, 51 Output: &File{ 52 NewName: "empty.txt", 53 NewMode: os.FileMode(0100644), 54 OldOIDPrefix: "0000000", 55 NewOIDPrefix: "e69de29", 56 IsNew: true, 57 }, 58 }, 59 "deleteFile": { 60 Input: `diff --git a/dir/file.txt b/dir/file.txt 61deleted file mode 100644 62index 44cc321..0000000 63--- a/dir/file.txt 64+++ /dev/null 65`, 66 Output: &File{ 67 OldName: "dir/file.txt", 68 OldMode: os.FileMode(0100644), 69 OldOIDPrefix: "44cc321", 70 NewOIDPrefix: "0000000", 71 IsDelete: true, 72 }, 73 }, 74 "changeMode": { 75 Input: `diff --git a/file.sh b/file.sh 76old mode 100644 77new mode 100755 78`, 79 Output: &File{ 80 OldName: "file.sh", 81 NewName: "file.sh", 82 OldMode: os.FileMode(0100644), 83 NewMode: os.FileMode(0100755), 84 }, 85 }, 86 "rename": { 87 Input: `diff --git a/foo.txt b/bar.txt 88similarity index 100% 89rename from foo.txt 90rename to bar.txt 91`, 92 Output: &File{ 93 OldName: "foo.txt", 94 NewName: "bar.txt", 95 Score: 100, 96 IsRename: true, 97 }, 98 }, 99 "copy": { 100 Input: `diff --git a/file.txt b/copy.txt 101similarity index 100% 102copy from file.txt 103copy to copy.txt 104`, 105 Output: &File{ 106 OldName: "file.txt", 107 NewName: "copy.txt", 108 Score: 100, 109 IsCopy: true, 110 }, 111 }, 112 "missingDefaultFilename": { 113 Input: `diff --git a/foo.sh b/bar.sh 114old mode 100644 115new mode 100755 116`, 117 Err: true, 118 }, 119 "missingNewFilename": { 120 Input: `diff --git a/file.txt b/file.txt 121index 1c23fcc..40a1b33 100644 122--- a/file.txt 123`, 124 Err: true, 125 }, 126 "missingOldFilename": { 127 Input: `diff --git a/file.txt b/file.txt 128index 1c23fcc..40a1b33 100644 129+++ b/file.txt 130`, 131 Err: true, 132 }, 133 "invalidHeaderLine": { 134 Input: `diff --git a/file.txt b/file.txt 135index deadbeef 136--- a/file.txt 137+++ b/file.txt 138`, 139 Err: true, 140 }, 141 "notGitHeader": { 142 Input: `--- file.txt 143+++ file.txt 144@@ -0,0 +1 @@ 145`, 146 Output: nil, 147 }, 148 } 149 150 for name, test := range tests { 151 t.Run(name, func(t *testing.T) { 152 p := newTestParser(test.Input, true) 153 154 f, err := p.ParseGitFileHeader() 155 if test.Err { 156 if err == nil || err == io.EOF { 157 t.Fatalf("expected error parsing git file header, got %v", err) 158 } 159 return 160 } 161 if err != nil { 162 t.Fatalf("unexpected error parsing git file header: %v", err) 163 } 164 165 if !reflect.DeepEqual(test.Output, f) { 166 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f) 167 } 168 }) 169 } 170} 171 172func TestParseTraditionalFileHeader(t *testing.T) { 173 tests := map[string]struct { 174 Input string 175 Output *File 176 Err bool 177 }{ 178 "fileContentChange": { 179 Input: `--- dir/file_old.txt 2019-03-21 23:00:00.0 -0700 180+++ dir/file_new.txt 2019-03-21 23:30:00.0 -0700 181@@ -0,0 +1 @@ 182`, 183 Output: &File{ 184 OldName: "dir/file_new.txt", 185 NewName: "dir/file_new.txt", 186 }, 187 }, 188 "newFile": { 189 Input: `--- /dev/null 1969-12-31 17:00:00.0 -0700 190+++ dir/file.txt 2019-03-21 23:30:00.0 -0700 191@@ -0,0 +1 @@ 192`, 193 Output: &File{ 194 NewName: "dir/file.txt", 195 IsNew: true, 196 }, 197 }, 198 "newFileTimestamp": { 199 Input: `--- dir/file.txt 1969-12-31 17:00:00.0 -0700 200+++ dir/file.txt 2019-03-21 23:30:00.0 -0700 201@@ -0,0 +1 @@ 202`, 203 Output: &File{ 204 NewName: "dir/file.txt", 205 IsNew: true, 206 }, 207 }, 208 "deleteFile": { 209 Input: `--- dir/file.txt 2019-03-21 23:30:00.0 -0700 210+++ /dev/null 1969-12-31 17:00:00.0 -0700 211@@ -0,0 +1 @@ 212`, 213 Output: &File{ 214 OldName: "dir/file.txt", 215 IsDelete: true, 216 }, 217 }, 218 "deleteFileTimestamp": { 219 Input: `--- dir/file.txt 2019-03-21 23:30:00.0 -0700 220+++ dir/file.txt 1969-12-31 17:00:00.0 -0700 221@@ -0,0 +1 @@ 222`, 223 Output: &File{ 224 OldName: "dir/file.txt", 225 IsDelete: true, 226 }, 227 }, 228 "useShortestPrefixName": { 229 Input: `--- dir/file.txt 2019-03-21 23:00:00.0 -0700 230+++ dir/file.txt~ 2019-03-21 23:30:00.0 -0700 231@@ -0,0 +1 @@ 232`, 233 Output: &File{ 234 OldName: "dir/file.txt", 235 NewName: "dir/file.txt", 236 }, 237 }, 238 "notTraditionalHeader": { 239 Input: `diff --git a/dir/file.txt b/dir/file.txt 240--- a/dir/file.txt 241+++ b/dir/file.txt 242`, 243 Output: nil, 244 }, 245 "noUnifiedFragment": { 246 Input: `--- dir/file_old.txt 2019-03-21 23:00:00.0 -0700 247+++ dir/file_new.txt 2019-03-21 23:30:00.0 -0700 248context line 249+added line 250`, 251 Output: nil, 252 }, 253 } 254 255 for name, test := range tests { 256 t.Run(name, func(t *testing.T) { 257 p := newTestParser(test.Input, true) 258 259 f, err := p.ParseTraditionalFileHeader() 260 if test.Err { 261 if err == nil || err == io.EOF { 262 t.Fatalf("expected error parsing traditional file header, got %v", err) 263 } 264 return 265 } 266 if err != nil { 267 t.Fatalf("unexpected error parsing traditional file header: %v", err) 268 } 269 270 if !reflect.DeepEqual(test.Output, f) { 271 t.Errorf("incorrect file\nexpected: %+v\n actual: %+v", test.Output, f) 272 } 273 }) 274 } 275} 276 277func TestCleanName(t *testing.T) { 278 tests := map[string]struct { 279 Input string 280 Drop int 281 Output string 282 }{ 283 "alreadyClean": { 284 Input: "a/b/c.txt", Output: "a/b/c.txt", 285 }, 286 "doubleSlashes": { 287 Input: "a//b/c.txt", Output: "a/b/c.txt", 288 }, 289 "tripleSlashes": { 290 Input: "a///b/c.txt", Output: "a/b/c.txt", 291 }, 292 "dropPrefix": { 293 Input: "a/b/c.txt", Drop: 2, Output: "c.txt", 294 }, 295 "removeDoublesBeforeDrop": { 296 Input: "a//b/c.txt", Drop: 1, Output: "b/c.txt", 297 }, 298 } 299 300 for name, test := range tests { 301 t.Run(name, func(t *testing.T) { 302 output := cleanName(test.Input, test.Drop) 303 if output != test.Output { 304 t.Fatalf("incorrect output: expected %q, actual %q", test.Output, output) 305 } 306 }) 307 } 308} 309 310func TestParseName(t *testing.T) { 311 tests := map[string]struct { 312 Input string 313 Term rune 314 Drop int 315 Output string 316 N int 317 Err bool 318 }{ 319 "singleUnquoted": { 320 Input: "dir/file.txt", Output: "dir/file.txt", N: 12, 321 }, 322 "singleQuoted": { 323 Input: `"dir/file.txt"`, Output: "dir/file.txt", N: 14, 324 }, 325 "quotedWithEscape": { 326 Input: `"dir/\"quotes\".txt"`, Output: `dir/"quotes".txt`, N: 20, 327 }, 328 "quotedWithSpaces": { 329 Input: `"dir/space file.txt"`, Output: "dir/space file.txt", N: 20, 330 }, 331 "tabTerminator": { 332 Input: "dir/space file.txt\tfile2.txt", Term: '\t', Output: "dir/space file.txt", N: 18, 333 }, 334 "dropPrefix": { 335 Input: "a/dir/file.txt", Drop: 1, Output: "dir/file.txt", N: 14, 336 }, 337 "multipleNames": { 338 Input: "dir/a.txt dir/b.txt", Term: -1, Output: "dir/a.txt", N: 9, 339 }, 340 "devNull": { 341 Input: "/dev/null", Term: '\t', Drop: 1, Output: "/dev/null", N: 9, 342 }, 343 "newlineAlwaysSeparates": { 344 Input: "dir/file.txt\n", Term: 0, Output: "dir/file.txt", N: 12, 345 }, 346 "emptyString": { 347 Input: "", Err: true, 348 }, 349 "emptyQuotedString": { 350 Input: `""`, Err: true, 351 }, 352 "unterminatedQuotes": { 353 Input: `"dir/file.txt`, Err: true, 354 }, 355 } 356 357 for name, test := range tests { 358 t.Run(name, func(t *testing.T) { 359 output, n, err := parseName(test.Input, test.Term, test.Drop) 360 if test.Err { 361 if err == nil || err == io.EOF { 362 t.Fatalf("expected error parsing name, but got %v", err) 363 } 364 return 365 } 366 if err != nil { 367 t.Fatalf("unexpected error parsing name: %v", err) 368 } 369 370 if output != test.Output { 371 t.Errorf("incorrect output: expected %q, actual: %q", test.Output, output) 372 } 373 if n != test.N { 374 t.Errorf("incorrect next position: expected %d, actual %d", test.N, n) 375 } 376 }) 377 } 378} 379 380func TestParseGitHeaderData(t *testing.T) { 381 tests := map[string]struct { 382 InputFile *File 383 Line string 384 DefaultName string 385 386 OutputFile *File 387 End bool 388 Err bool 389 }{ 390 "fragementEndsParsing": { 391 Line: "@@ -12,3 +12,2 @@\n", 392 End: true, 393 }, 394 "unknownEndsParsing": { 395 Line: "GIT binary file\n", 396 End: true, 397 }, 398 "oldFileName": { 399 Line: "--- a/dir/file.txt\n", 400 OutputFile: &File{ 401 OldName: "dir/file.txt", 402 }, 403 }, 404 "oldFileNameDevNull": { 405 InputFile: &File{ 406 IsNew: true, 407 }, 408 Line: "--- /dev/null\n", 409 OutputFile: &File{ 410 IsNew: true, 411 }, 412 }, 413 "oldFileNameInconsistent": { 414 InputFile: &File{ 415 OldName: "dir/foo.txt", 416 }, 417 Line: "--- a/dir/bar.txt\n", 418 Err: true, 419 }, 420 "oldFileNameExistingCreateMismatch": { 421 InputFile: &File{ 422 OldName: "dir/foo.txt", 423 IsNew: true, 424 }, 425 Line: "--- /dev/null\n", 426 Err: true, 427 }, 428 "oldFileNameParsedCreateMismatch": { 429 InputFile: &File{ 430 IsNew: true, 431 }, 432 Line: "--- a/dir/file.txt\n", 433 Err: true, 434 }, 435 "oldFileNameMissing": { 436 Line: "--- \n", 437 Err: true, 438 }, 439 "newFileName": { 440 Line: "+++ b/dir/file.txt\n", 441 OutputFile: &File{ 442 NewName: "dir/file.txt", 443 }, 444 }, 445 "newFileNameDevNull": { 446 InputFile: &File{ 447 IsDelete: true, 448 }, 449 Line: "+++ /dev/null\n", 450 OutputFile: &File{ 451 IsDelete: true, 452 }, 453 }, 454 "newFileNameInconsistent": { 455 InputFile: &File{ 456 NewName: "dir/foo.txt", 457 }, 458 Line: "+++ b/dir/bar.txt\n", 459 Err: true, 460 }, 461 "newFileNameExistingDeleteMismatch": { 462 InputFile: &File{ 463 NewName: "dir/foo.txt", 464 IsDelete: true, 465 }, 466 Line: "+++ /dev/null\n", 467 Err: true, 468 }, 469 "newFileNameParsedDeleteMismatch": { 470 InputFile: &File{ 471 IsDelete: true, 472 }, 473 Line: "+++ b/dir/file.txt\n", 474 Err: true, 475 }, 476 "newFileNameMissing": { 477 Line: "+++ \n", 478 Err: true, 479 }, 480 "oldMode": { 481 Line: "old mode 100644\n", 482 OutputFile: &File{ 483 OldMode: os.FileMode(0100644), 484 }, 485 }, 486 "invalidOldMode": { 487 Line: "old mode rw\n", 488 Err: true, 489 }, 490 "newMode": { 491 Line: "new mode 100755\n", 492 OutputFile: &File{ 493 NewMode: os.FileMode(0100755), 494 }, 495 }, 496 "invalidNewMode": { 497 Line: "new mode rwx\n", 498 Err: true, 499 }, 500 "deletedFileMode": { 501 Line: "deleted file mode 100644\n", 502 DefaultName: "dir/file.txt", 503 OutputFile: &File{ 504 OldName: "dir/file.txt", 505 OldMode: os.FileMode(0100644), 506 IsDelete: true, 507 }, 508 }, 509 "newFileMode": { 510 Line: "new file mode 100755\n", 511 DefaultName: "dir/file.txt", 512 OutputFile: &File{ 513 NewName: "dir/file.txt", 514 NewMode: os.FileMode(0100755), 515 IsNew: true, 516 }, 517 }, 518 "copyFrom": { 519 Line: "copy from dir/file.txt\n", 520 OutputFile: &File{ 521 OldName: "dir/file.txt", 522 IsCopy: true, 523 }, 524 }, 525 "copyTo": { 526 Line: "copy to dir/file.txt\n", 527 OutputFile: &File{ 528 NewName: "dir/file.txt", 529 IsCopy: true, 530 }, 531 }, 532 "renameFrom": { 533 Line: "rename from dir/file.txt\n", 534 OutputFile: &File{ 535 OldName: "dir/file.txt", 536 IsRename: true, 537 }, 538 }, 539 "renameTo": { 540 Line: "rename to dir/file.txt\n", 541 OutputFile: &File{ 542 NewName: "dir/file.txt", 543 IsRename: true, 544 }, 545 }, 546 "similarityIndex": { 547 Line: "similarity index 88%\n", 548 OutputFile: &File{ 549 Score: 88, 550 }, 551 }, 552 "similarityIndexTooBig": { 553 Line: "similarity index 9001%\n", 554 OutputFile: &File{ 555 Score: 0, 556 }, 557 }, 558 "similarityIndexInvalid": { 559 Line: "similarity index 12ab%\n", 560 Err: true, 561 }, 562 "indexFullSHA1AndMode": { 563 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098 100644\n", 564 OutputFile: &File{ 565 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 566 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 567 OldMode: os.FileMode(0100644), 568 }, 569 }, 570 "indexFullSHA1NoMode": { 571 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098\n", 572 OutputFile: &File{ 573 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 574 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 575 }, 576 }, 577 "indexAbbrevSHA1AndMode": { 578 Line: "index 79c6d7..04fab9 100644\n", 579 OutputFile: &File{ 580 OldOIDPrefix: "79c6d7", 581 NewOIDPrefix: "04fab9", 582 OldMode: os.FileMode(0100644), 583 }, 584 }, 585 "indexInvalid": { 586 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14\n", 587 Err: true, 588 }, 589 } 590 591 for name, test := range tests { 592 t.Run(name, func(t *testing.T) { 593 var f File 594 if test.InputFile != nil { 595 f = *test.InputFile 596 } 597 598 end, err := parseGitHeaderData(&f, test.Line, test.DefaultName) 599 if test.Err { 600 if err == nil || err == io.EOF { 601 t.Fatalf("expected error parsing header data, but got %v", err) 602 } 603 return 604 } 605 if err != nil { 606 t.Fatalf("unexpected error parsing header data: %v", err) 607 } 608 609 if test.OutputFile != nil && !reflect.DeepEqual(test.OutputFile, &f) { 610 t.Errorf("incorrect output:\nexpected: %+v\nactual: %+v", test.OutputFile, &f) 611 } 612 if end != test.End { 613 t.Errorf("incorrect end state, expected %t, actual %t", test.End, end) 614 } 615 }) 616 } 617} 618 619func TestParseGitHeaderName(t *testing.T) { 620 tests := map[string]struct { 621 Input string 622 Output string 623 Err bool 624 }{ 625 "twoMatchingNames": { 626 Input: "a/dir/file.txt b/dir/file.txt", 627 Output: "dir/file.txt", 628 }, 629 "twoDifferentNames": { 630 Input: "a/dir/foo.txt b/dir/bar.txt", 631 Output: "", 632 }, 633 "missingSecondName": { 634 Input: "a/dir/foo.txt", 635 Err: true, 636 }, 637 "invalidName": { 638 Input: `"a/dir/file.txt b/dir/file.txt`, 639 Err: true, 640 }, 641 } 642 643 for name, test := range tests { 644 t.Run(name, func(t *testing.T) { 645 output, err := parseGitHeaderName(test.Input) 646 if test.Err { 647 if err == nil { 648 t.Fatalf("expected error parsing header name, but got nil") 649 } 650 return 651 } 652 if err != nil { 653 t.Fatalf("unexpected error parsing header name: %v", err) 654 } 655 656 if output != test.Output { 657 t.Errorf("incorrect output: expected %q, actual %q", test.Output, output) 658 } 659 }) 660 } 661} 662 663func TestHasEpochTimestamp(t *testing.T) { 664 tests := map[string]struct { 665 Input string 666 Output bool 667 }{ 668 "utcTimestamp": { 669 Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n", 670 Output: true, 671 }, 672 "utcZoneWithColon": { 673 Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n", 674 Output: true, 675 }, 676 "utcZoneWithMilliseconds": { 677 Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n", 678 Output: true, 679 }, 680 "westTimestamp": { 681 Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n", 682 Output: true, 683 }, 684 "eastTimestamp": { 685 Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n", 686 Output: true, 687 }, 688 "noTab": { 689 Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n", 690 Output: false, 691 }, 692 "invalidFormat": { 693 Input: "+++ file.txt\t1970-01-01T00:00:00Z\n", 694 Output: false, 695 }, 696 "notEpoch": { 697 Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n", 698 Output: false, 699 }, 700 } 701 702 for name, test := range tests { 703 t.Run(name, func(t *testing.T) { 704 output := hasEpochTimestamp(test.Input) 705 if output != test.Output { 706 t.Errorf("incorrect output: expected %t, actual %t", test.Output, output) 707 } 708 }) 709 } 710}