fork of go-gitdiff with jj support
at v0.7.4 17 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 byte 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 "unquotedWithSpaces": { 338 Input: "dir/with spaces.txt", Output: "dir/with spaces.txt", N: 19, 339 }, 340 "unquotedWithTrailingSpaces": { 341 Input: "dir/with spaces.space ", Output: "dir/with spaces.space ", N: 23, 342 }, 343 "devNull": { 344 Input: "/dev/null", Term: '\t', Drop: 1, Output: "/dev/null", N: 9, 345 }, 346 "newlineSeparates": { 347 Input: "dir/file.txt\n", Output: "dir/file.txt", N: 12, 348 }, 349 "emptyString": { 350 Input: "", Err: true, 351 }, 352 "emptyQuotedString": { 353 Input: `""`, Err: true, 354 }, 355 "unterminatedQuotes": { 356 Input: `"dir/file.txt`, Err: true, 357 }, 358 } 359 360 for name, test := range tests { 361 t.Run(name, func(t *testing.T) { 362 output, n, err := parseName(test.Input, test.Term, test.Drop) 363 if test.Err { 364 if err == nil || err == io.EOF { 365 t.Fatalf("expected error parsing name, but got %v", err) 366 } 367 return 368 } 369 if err != nil { 370 t.Fatalf("unexpected error parsing name: %v", err) 371 } 372 373 if output != test.Output { 374 t.Errorf("incorrect output: expected %q, actual: %q", test.Output, output) 375 } 376 if n != test.N { 377 t.Errorf("incorrect next position: expected %d, actual %d", test.N, n) 378 } 379 }) 380 } 381} 382 383func TestParseGitHeaderData(t *testing.T) { 384 tests := map[string]struct { 385 InputFile *File 386 Line string 387 DefaultName string 388 389 OutputFile *File 390 End bool 391 Err bool 392 }{ 393 "fragementEndsParsing": { 394 Line: "@@ -12,3 +12,2 @@\n", 395 End: true, 396 }, 397 "unknownEndsParsing": { 398 Line: "GIT binary file\n", 399 End: true, 400 }, 401 "oldFileName": { 402 Line: "--- a/dir/file.txt\n", 403 OutputFile: &File{ 404 OldName: "dir/file.txt", 405 }, 406 }, 407 "oldFileNameDevNull": { 408 InputFile: &File{ 409 IsNew: true, 410 }, 411 Line: "--- /dev/null\n", 412 OutputFile: &File{ 413 IsNew: true, 414 }, 415 }, 416 "oldFileNameInconsistent": { 417 InputFile: &File{ 418 OldName: "dir/foo.txt", 419 }, 420 Line: "--- a/dir/bar.txt\n", 421 Err: true, 422 }, 423 "oldFileNameExistingCreateMismatch": { 424 InputFile: &File{ 425 OldName: "dir/foo.txt", 426 IsNew: true, 427 }, 428 Line: "--- /dev/null\n", 429 Err: true, 430 }, 431 "oldFileNameParsedCreateMismatch": { 432 InputFile: &File{ 433 IsNew: true, 434 }, 435 Line: "--- a/dir/file.txt\n", 436 Err: true, 437 }, 438 "oldFileNameMissing": { 439 Line: "--- \n", 440 Err: true, 441 }, 442 "newFileName": { 443 Line: "+++ b/dir/file.txt\n", 444 OutputFile: &File{ 445 NewName: "dir/file.txt", 446 }, 447 }, 448 "newFileNameDevNull": { 449 InputFile: &File{ 450 IsDelete: true, 451 }, 452 Line: "+++ /dev/null\n", 453 OutputFile: &File{ 454 IsDelete: true, 455 }, 456 }, 457 "newFileNameInconsistent": { 458 InputFile: &File{ 459 NewName: "dir/foo.txt", 460 }, 461 Line: "+++ b/dir/bar.txt\n", 462 Err: true, 463 }, 464 "newFileNameExistingDeleteMismatch": { 465 InputFile: &File{ 466 NewName: "dir/foo.txt", 467 IsDelete: true, 468 }, 469 Line: "+++ /dev/null\n", 470 Err: true, 471 }, 472 "newFileNameParsedDeleteMismatch": { 473 InputFile: &File{ 474 IsDelete: true, 475 }, 476 Line: "+++ b/dir/file.txt\n", 477 Err: true, 478 }, 479 "newFileNameMissing": { 480 Line: "+++ \n", 481 Err: true, 482 }, 483 "oldMode": { 484 Line: "old mode 100644\n", 485 OutputFile: &File{ 486 OldMode: os.FileMode(0100644), 487 }, 488 }, 489 "oldModeWithTrailingSpace": { 490 Line: "old mode 100644\r\n", 491 OutputFile: &File{ 492 OldMode: os.FileMode(0100644), 493 }, 494 }, 495 "invalidOldMode": { 496 Line: "old mode rw\n", 497 Err: true, 498 }, 499 "newMode": { 500 Line: "new mode 100755\n", 501 OutputFile: &File{ 502 NewMode: os.FileMode(0100755), 503 }, 504 }, 505 "newModeWithTrailingSpace": { 506 Line: "new mode 100755\r\n", 507 OutputFile: &File{ 508 NewMode: os.FileMode(0100755), 509 }, 510 }, 511 "invalidNewMode": { 512 Line: "new mode rwx\n", 513 Err: true, 514 }, 515 "deletedFileMode": { 516 Line: "deleted file mode 100644\n", 517 DefaultName: "dir/file.txt", 518 OutputFile: &File{ 519 OldName: "dir/file.txt", 520 OldMode: os.FileMode(0100644), 521 IsDelete: true, 522 }, 523 }, 524 "newFileMode": { 525 Line: "new file mode 100755\n", 526 DefaultName: "dir/file.txt", 527 OutputFile: &File{ 528 NewName: "dir/file.txt", 529 NewMode: os.FileMode(0100755), 530 IsNew: true, 531 }, 532 }, 533 "newFileModeWithTrailingSpace": { 534 Line: "new file mode 100755\r\n", 535 DefaultName: "dir/file.txt", 536 OutputFile: &File{ 537 NewName: "dir/file.txt", 538 NewMode: os.FileMode(0100755), 539 IsNew: true, 540 }, 541 }, 542 "copyFrom": { 543 Line: "copy from dir/file.txt\n", 544 OutputFile: &File{ 545 OldName: "dir/file.txt", 546 IsCopy: true, 547 }, 548 }, 549 "copyTo": { 550 Line: "copy to dir/file.txt\n", 551 OutputFile: &File{ 552 NewName: "dir/file.txt", 553 IsCopy: true, 554 }, 555 }, 556 "renameFrom": { 557 Line: "rename from dir/file.txt\n", 558 OutputFile: &File{ 559 OldName: "dir/file.txt", 560 IsRename: true, 561 }, 562 }, 563 "renameTo": { 564 Line: "rename to dir/file.txt\n", 565 OutputFile: &File{ 566 NewName: "dir/file.txt", 567 IsRename: true, 568 }, 569 }, 570 "similarityIndex": { 571 Line: "similarity index 88%\n", 572 OutputFile: &File{ 573 Score: 88, 574 }, 575 }, 576 "similarityIndexTooBig": { 577 Line: "similarity index 9001%\n", 578 OutputFile: &File{ 579 Score: 0, 580 }, 581 }, 582 "similarityIndexInvalid": { 583 Line: "similarity index 12ab%\n", 584 Err: true, 585 }, 586 "indexFullSHA1AndMode": { 587 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098 100644\n", 588 OutputFile: &File{ 589 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 590 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 591 OldMode: os.FileMode(0100644), 592 }, 593 }, 594 "indexFullSHA1NoMode": { 595 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098\n", 596 OutputFile: &File{ 597 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 598 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 599 }, 600 }, 601 "indexAbbrevSHA1AndMode": { 602 Line: "index 79c6d7..04fab9 100644\n", 603 OutputFile: &File{ 604 OldOIDPrefix: "79c6d7", 605 NewOIDPrefix: "04fab9", 606 OldMode: os.FileMode(0100644), 607 }, 608 }, 609 "indexInvalid": { 610 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14\n", 611 Err: true, 612 }, 613 } 614 615 for name, test := range tests { 616 t.Run(name, func(t *testing.T) { 617 var f File 618 if test.InputFile != nil { 619 f = *test.InputFile 620 } 621 622 end, err := parseGitHeaderData(&f, test.Line, test.DefaultName) 623 if test.Err { 624 if err == nil || err == io.EOF { 625 t.Fatalf("expected error parsing header data, but got %v", err) 626 } 627 return 628 } 629 if err != nil { 630 t.Fatalf("unexpected error parsing header data: %v", err) 631 } 632 633 if test.OutputFile != nil && !reflect.DeepEqual(test.OutputFile, &f) { 634 t.Errorf("incorrect output:\nexpected: %+v\nactual: %+v", test.OutputFile, &f) 635 } 636 if end != test.End { 637 t.Errorf("incorrect end state, expected %t, actual %t", test.End, end) 638 } 639 }) 640 } 641} 642 643func TestParseGitHeaderName(t *testing.T) { 644 tests := map[string]struct { 645 Input string 646 Output string 647 Err bool 648 }{ 649 "twoMatchingNames": { 650 Input: "a/dir/file.txt b/dir/file.txt", 651 Output: "dir/file.txt", 652 }, 653 "twoDifferentNames": { 654 Input: "a/dir/foo.txt b/dir/bar.txt", 655 Output: "", 656 }, 657 "matchingNamesWithSpaces": { 658 Input: "a/dir/file with spaces.txt b/dir/file with spaces.txt", 659 Output: "dir/file with spaces.txt", 660 }, 661 "matchingNamesWithTrailingSpaces": { 662 Input: "a/dir/spaces b/dir/spaces ", 663 Output: "dir/spaces ", 664 }, 665 "matchingNamesQuoted": { 666 Input: `"a/dir/\"quotes\".txt" "b/dir/\"quotes\".txt"`, 667 Output: `dir/"quotes".txt`, 668 }, 669 "matchingNamesFirstQuoted": { 670 Input: `"a/dir/file.txt" b/dir/file.txt`, 671 Output: "dir/file.txt", 672 }, 673 "matchingNamesSecondQuoted": { 674 Input: `a/dir/file.txt "b/dir/file.txt"`, 675 Output: "dir/file.txt", 676 }, 677 "noSecondName": { 678 Input: "a/dir/foo.txt", 679 Output: "", 680 }, 681 "noSecondNameQuoted": { 682 Input: `"a/dir/foo.txt"`, 683 Output: "", 684 }, 685 "invalidName": { 686 Input: `"a/dir/file.txt b/dir/file.txt`, 687 Err: true, 688 }, 689 } 690 691 for name, test := range tests { 692 t.Run(name, func(t *testing.T) { 693 output, err := parseGitHeaderName(test.Input) 694 if test.Err { 695 if err == nil { 696 t.Fatalf("expected error parsing header name, but got nil") 697 } 698 return 699 } 700 if err != nil { 701 t.Fatalf("unexpected error parsing header name: %v", err) 702 } 703 704 if output != test.Output { 705 t.Errorf("incorrect output: expected %q, actual %q", test.Output, output) 706 } 707 }) 708 } 709} 710 711func TestHasEpochTimestamp(t *testing.T) { 712 tests := map[string]struct { 713 Input string 714 Output bool 715 }{ 716 "utcTimestamp": { 717 Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n", 718 Output: true, 719 }, 720 "utcZoneWithColon": { 721 Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n", 722 Output: true, 723 }, 724 "utcZoneWithMilliseconds": { 725 Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n", 726 Output: true, 727 }, 728 "westTimestamp": { 729 Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n", 730 Output: true, 731 }, 732 "eastTimestamp": { 733 Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n", 734 Output: true, 735 }, 736 "noTab": { 737 Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n", 738 Output: false, 739 }, 740 "invalidFormat": { 741 Input: "+++ file.txt\t1970-01-01T00:00:00Z\n", 742 Output: false, 743 }, 744 "notEpoch": { 745 Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n", 746 Output: false, 747 }, 748 "notTimestamp": { 749 Input: "+++ file.txt\trandom text\n", 750 Output: false, 751 }, 752 "notTimestampShort": { 753 Input: "+++ file.txt\t0\n", 754 Output: false, 755 }, 756 } 757 758 for name, test := range tests { 759 t.Run(name, func(t *testing.T) { 760 output := hasEpochTimestamp(test.Input) 761 if output != test.Output { 762 t.Errorf("incorrect output: expected %t, actual %t", test.Output, output) 763 } 764 }) 765 } 766}