fork of go-gitdiff with jj support
at v0.7.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 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 "invalidOldMode": { 490 Line: "old mode rw\n", 491 Err: true, 492 }, 493 "newMode": { 494 Line: "new mode 100755\n", 495 OutputFile: &File{ 496 NewMode: os.FileMode(0100755), 497 }, 498 }, 499 "invalidNewMode": { 500 Line: "new mode rwx\n", 501 Err: true, 502 }, 503 "deletedFileMode": { 504 Line: "deleted file mode 100644\n", 505 DefaultName: "dir/file.txt", 506 OutputFile: &File{ 507 OldName: "dir/file.txt", 508 OldMode: os.FileMode(0100644), 509 IsDelete: true, 510 }, 511 }, 512 "newFileMode": { 513 Line: "new file mode 100755\n", 514 DefaultName: "dir/file.txt", 515 OutputFile: &File{ 516 NewName: "dir/file.txt", 517 NewMode: os.FileMode(0100755), 518 IsNew: true, 519 }, 520 }, 521 "copyFrom": { 522 Line: "copy from dir/file.txt\n", 523 OutputFile: &File{ 524 OldName: "dir/file.txt", 525 IsCopy: true, 526 }, 527 }, 528 "copyTo": { 529 Line: "copy to dir/file.txt\n", 530 OutputFile: &File{ 531 NewName: "dir/file.txt", 532 IsCopy: true, 533 }, 534 }, 535 "renameFrom": { 536 Line: "rename from dir/file.txt\n", 537 OutputFile: &File{ 538 OldName: "dir/file.txt", 539 IsRename: true, 540 }, 541 }, 542 "renameTo": { 543 Line: "rename to dir/file.txt\n", 544 OutputFile: &File{ 545 NewName: "dir/file.txt", 546 IsRename: true, 547 }, 548 }, 549 "similarityIndex": { 550 Line: "similarity index 88%\n", 551 OutputFile: &File{ 552 Score: 88, 553 }, 554 }, 555 "similarityIndexTooBig": { 556 Line: "similarity index 9001%\n", 557 OutputFile: &File{ 558 Score: 0, 559 }, 560 }, 561 "similarityIndexInvalid": { 562 Line: "similarity index 12ab%\n", 563 Err: true, 564 }, 565 "indexFullSHA1AndMode": { 566 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098 100644\n", 567 OutputFile: &File{ 568 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 569 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 570 OldMode: os.FileMode(0100644), 571 }, 572 }, 573 "indexFullSHA1NoMode": { 574 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14..04fab916d8f938173cbb8b93469855f0e838f098\n", 575 OutputFile: &File{ 576 OldOIDPrefix: "79c6d7f7b7e76c75b3d238f12fb1323f2333ba14", 577 NewOIDPrefix: "04fab916d8f938173cbb8b93469855f0e838f098", 578 }, 579 }, 580 "indexAbbrevSHA1AndMode": { 581 Line: "index 79c6d7..04fab9 100644\n", 582 OutputFile: &File{ 583 OldOIDPrefix: "79c6d7", 584 NewOIDPrefix: "04fab9", 585 OldMode: os.FileMode(0100644), 586 }, 587 }, 588 "indexInvalid": { 589 Line: "index 79c6d7f7b7e76c75b3d238f12fb1323f2333ba14\n", 590 Err: true, 591 }, 592 } 593 594 for name, test := range tests { 595 t.Run(name, func(t *testing.T) { 596 var f File 597 if test.InputFile != nil { 598 f = *test.InputFile 599 } 600 601 end, err := parseGitHeaderData(&f, test.Line, test.DefaultName) 602 if test.Err { 603 if err == nil || err == io.EOF { 604 t.Fatalf("expected error parsing header data, but got %v", err) 605 } 606 return 607 } 608 if err != nil { 609 t.Fatalf("unexpected error parsing header data: %v", err) 610 } 611 612 if test.OutputFile != nil && !reflect.DeepEqual(test.OutputFile, &f) { 613 t.Errorf("incorrect output:\nexpected: %+v\nactual: %+v", test.OutputFile, &f) 614 } 615 if end != test.End { 616 t.Errorf("incorrect end state, expected %t, actual %t", test.End, end) 617 } 618 }) 619 } 620} 621 622func TestParseGitHeaderName(t *testing.T) { 623 tests := map[string]struct { 624 Input string 625 Output string 626 Err bool 627 }{ 628 "twoMatchingNames": { 629 Input: "a/dir/file.txt b/dir/file.txt", 630 Output: "dir/file.txt", 631 }, 632 "twoDifferentNames": { 633 Input: "a/dir/foo.txt b/dir/bar.txt", 634 Output: "", 635 }, 636 "matchingNamesWithSpaces": { 637 Input: "a/dir/file with spaces.txt b/dir/file with spaces.txt", 638 Output: "dir/file with spaces.txt", 639 }, 640 "matchingNamesWithTrailingSpaces": { 641 Input: "a/dir/spaces b/dir/spaces ", 642 Output: "dir/spaces ", 643 }, 644 "matchingNamesQuoted": { 645 Input: `"a/dir/\"quotes\".txt" "b/dir/\"quotes\".txt"`, 646 Output: `dir/"quotes".txt`, 647 }, 648 "matchingNamesFirstQuoted": { 649 Input: `"a/dir/file.txt" b/dir/file.txt`, 650 Output: "dir/file.txt", 651 }, 652 "matchingNamesSecondQuoted": { 653 Input: `a/dir/file.txt "b/dir/file.txt"`, 654 Output: "dir/file.txt", 655 }, 656 "noSecondName": { 657 Input: "a/dir/foo.txt", 658 Output: "", 659 }, 660 "noSecondNameQuoted": { 661 Input: `"a/dir/foo.txt"`, 662 Output: "", 663 }, 664 "invalidName": { 665 Input: `"a/dir/file.txt b/dir/file.txt`, 666 Err: true, 667 }, 668 } 669 670 for name, test := range tests { 671 t.Run(name, func(t *testing.T) { 672 output, err := parseGitHeaderName(test.Input) 673 if test.Err { 674 if err == nil { 675 t.Fatalf("expected error parsing header name, but got nil") 676 } 677 return 678 } 679 if err != nil { 680 t.Fatalf("unexpected error parsing header name: %v", err) 681 } 682 683 if output != test.Output { 684 t.Errorf("incorrect output: expected %q, actual %q", test.Output, output) 685 } 686 }) 687 } 688} 689 690func TestHasEpochTimestamp(t *testing.T) { 691 tests := map[string]struct { 692 Input string 693 Output bool 694 }{ 695 "utcTimestamp": { 696 Input: "+++ file.txt\t1970-01-01 00:00:00 +0000\n", 697 Output: true, 698 }, 699 "utcZoneWithColon": { 700 Input: "+++ file.txt\t1970-01-01 00:00:00 +00:00\n", 701 Output: true, 702 }, 703 "utcZoneWithMilliseconds": { 704 Input: "+++ file.txt\t1970-01-01 00:00:00.000000 +00:00\n", 705 Output: true, 706 }, 707 "westTimestamp": { 708 Input: "+++ file.txt\t1969-12-31 16:00:00 -0800\n", 709 Output: true, 710 }, 711 "eastTimestamp": { 712 Input: "+++ file.txt\t1970-01-01 04:00:00 +0400\n", 713 Output: true, 714 }, 715 "noTab": { 716 Input: "+++ file.txt 1970-01-01 00:00:00 +0000\n", 717 Output: false, 718 }, 719 "invalidFormat": { 720 Input: "+++ file.txt\t1970-01-01T00:00:00Z\n", 721 Output: false, 722 }, 723 "notEpoch": { 724 Input: "+++ file.txt\t2019-03-21 12:34:56.789 -0700\n", 725 Output: false, 726 }, 727 "notTimestamp": { 728 Input: "+++ file.txt\trandom text\n", 729 Output: false, 730 }, 731 "notTimestampShort": { 732 Input: "+++ file.txt\t0\n", 733 Output: false, 734 }, 735 } 736 737 for name, test := range tests { 738 t.Run(name, func(t *testing.T) { 739 output := hasEpochTimestamp(test.Input) 740 if output != test.Output { 741 t.Errorf("incorrect output: expected %t, actual %t", test.Output, output) 742 } 743 }) 744 } 745}