github.com/boyter/gocodewalker@v1.3.2/go-gitignore/repository_test.go (about) 1 // SPDX-License-Identifier: MIT 2 3 package gitignore_test 4 5 import ( 6 "github.com/boyter/gocodewalker/go-gitignore" 7 "os" 8 "path/filepath" 9 "testing" 10 ) 11 12 type repositorytest struct { 13 file string 14 directory string 15 cache gitignore.Cache 16 cached bool 17 error func(e gitignore.Error) bool 18 errors []gitignore.Error 19 bad int 20 instance func(string) (gitignore.GitIgnore, error) 21 exclude string 22 gitdir string 23 } // repostorytest{} 24 25 func (r *repositorytest) create(path string, gitdir bool) (gitignore.GitIgnore, error) { 26 // if we have an error handler, reset the list of errors 27 if r.error != nil { 28 r.errors = make([]gitignore.Error, 0) 29 } 30 31 if r.file == gitignore.File || r.file == "" { 32 // should we create the global exclude file 33 r.gitdir = os.Getenv("GIT_DIR") 34 if gitdir { 35 // create a temporary file for the global exclude file 36 _exclude, _err := exclude(_GITEXCLUDE) 37 if _err != nil { 38 return nil, _err 39 } 40 41 // extract the current value of the GIT_DIR environment variable 42 // and set the value to be that of the temporary file 43 r.exclude = _exclude 44 _err = os.Setenv("GIT_DIR", r.exclude) 45 if _err != nil { 46 return nil, _err 47 } 48 } else { 49 _err := os.Unsetenv("GIT_DIR") 50 if _err != nil { 51 return nil, _err 52 } 53 } 54 } 55 56 // attempt to create the GitIgnore instance 57 _repository, _err := r.instance(path) 58 59 // if we encountered errors, and the first error has a zero position 60 // then it represents a file access error 61 // - extract the error and return it 62 // - remove it from the list of errors 63 if len(r.errors) > 0 { 64 if r.errors[0].Position().Zero() { 65 _err = r.errors[0].Underlying() 66 r.errors = r.errors[1:] 67 } 68 } 69 70 // return the GitIgnore instance 71 return _repository, _err 72 } // create() 73 74 func (r *repositorytest) destroy() { 75 // remove the temporary files and directories 76 for _, _path := range []string{r.directory, r.exclude} { 77 if _path != "" { 78 defer os.RemoveAll(_path) 79 } 80 } 81 82 if r.file == gitignore.File || r.file == "" { 83 // reset the GIT_DIR environment variable 84 if r.gitdir == "" { 85 defer os.Unsetenv("GIT_DIR") 86 } else { 87 defer os.Setenv("GIT_DIR", r.gitdir) 88 } 89 } 90 } // destroy() 91 92 type invalidtest struct { 93 *repositorytest 94 tag string 95 match func() gitignore.Match 96 } // invalidtest{} 97 98 func TestRepository(t *testing.T) { 99 _test := &repositorytest{} 100 _test.bad = _GITREPOSITORYERRORS 101 _test.instance = func(path string) (gitignore.GitIgnore, error) { 102 return gitignore.NewRepository(path) 103 } 104 105 // perform the repository tests 106 repository(t, _test, _REPOSITORYMATCHES) 107 108 // remove the temporary directory used for this test 109 defer _test.destroy() 110 } // TestRepository() 111 112 func TestRepositoryWithFile(t *testing.T) { 113 _test := &repositorytest{} 114 _test.bad = _GITREPOSITORYERRORS 115 _test.file = gitignore.File + "-with-file" 116 _test.instance = func(path string) (gitignore.GitIgnore, error) { 117 return gitignore.NewRepositoryWithFile(path, _test.file) 118 } 119 120 // perform the repository tests 121 repository(t, _test, _REPOSITORYMATCHES) 122 123 // remove the temporary directory used for this test 124 defer _test.destroy() 125 } // TestRepositoryWithFile() 126 127 func TestRepositoryWithErrors(t *testing.T) { 128 _test := &repositorytest{} 129 _test.bad = _GITREPOSITORYERRORS 130 _test.file = gitignore.File + "-with-errors" 131 _test.error = func(e gitignore.Error) bool { 132 _test.errors = append(_test.errors, e) 133 return true 134 } 135 _test.instance = func(path string) (gitignore.GitIgnore, error) { 136 return gitignore.NewRepositoryWithErrors( 137 path, _test.file, _test.error, 138 ), nil 139 } 140 141 // perform the repository tests 142 repository(t, _test, _REPOSITORYMATCHES) 143 144 // remove the temporary directory used for this test 145 defer _test.destroy() 146 } // TestRepositoryWithErrors() 147 148 func TestRepositoryWithErrorsFalse(t *testing.T) { 149 _test := &repositorytest{} 150 _test.bad = _GITREPOSITORYERRORSFALSE 151 _test.file = gitignore.File + "-with-errors-false" 152 _test.error = func(e gitignore.Error) bool { 153 _test.errors = append(_test.errors, e) 154 return false 155 } 156 _test.instance = func(path string) (gitignore.GitIgnore, error) { 157 return gitignore.NewRepositoryWithErrors( 158 path, _test.file, _test.error, 159 ), nil 160 } 161 162 // perform the repository tests 163 repository(t, _test, _REPOSITORYMATCHESFALSE) 164 165 // remove the temporary directory used for this test 166 defer _test.destroy() 167 } // TestRepositoryWithErrorsFalse() 168 169 func TestRepositoryWithCache(t *testing.T) { 170 _test := &repositorytest{} 171 _test.bad = _GITREPOSITORYERRORS 172 _test.cache = gitignore.NewCache() 173 _test.cached = true 174 _test.instance = func(path string) (gitignore.GitIgnore, error) { 175 return gitignore.NewRepositoryWithCache( 176 path, _test.file, _test.cache, _test.error, 177 ), nil 178 } 179 180 // perform the repository tests 181 repository(t, _test, _REPOSITORYMATCHES) 182 183 // clean up 184 defer _test.destroy() 185 186 // rerun the tests while accumulating errors 187 _test.directory = "" 188 _test.file = gitignore.File + "-with-cache" 189 _test.error = func(e gitignore.Error) bool { 190 _test.errors = append(_test.errors, e) 191 return true 192 } 193 repository(t, _test, _REPOSITORYMATCHES) 194 195 // remove the temporary directory used for this test 196 _err := os.RemoveAll(_test.directory) 197 if _err != nil { 198 t.Fatalf( 199 "unable to remove temporary directory %s: %s", 200 _test.directory, _err.Error(), 201 ) 202 } 203 204 // recreate the temporary directory 205 // - this remove & recreate gives us an empty directory for the 206 // repository test 207 // - this lets us test the caching 208 _err = os.MkdirAll(_test.directory, _GITMASK) 209 if _err != nil { 210 t.Fatalf( 211 "unable to recreate temporary directory %s: %s", 212 _test.directory, _err.Error(), 213 ) 214 } 215 defer _test.destroy() 216 217 // repeat the repository tests 218 // - these should succeed using just the cache data 219 repository(t, _test, _REPOSITORYMATCHES) 220 } // TestRepositoryWithCache() 221 222 func TestInvalidRepository(t *testing.T) { 223 _test := &repositorytest{} 224 _test.instance = func(path string) (gitignore.GitIgnore, error) { 225 return gitignore.NewRepository(path) 226 } 227 228 // perform the invalid repository tests 229 invalid(t, _test) 230 } // TestInvalidRepository() 231 232 func TestInvalidRepositoryWithFile(t *testing.T) { 233 _test := &repositorytest{} 234 _test.file = gitignore.File + "-invalid-with-file" 235 _test.instance = func(path string) (gitignore.GitIgnore, error) { 236 return gitignore.NewRepositoryWithFile(path, _test.file) 237 } 238 239 // perform the invalid repository tests 240 invalid(t, _test) 241 } // TestInvalidRepositoryWithFile() 242 243 func TestInvalidRepositoryWithErrors(t *testing.T) { 244 _test := &repositorytest{} 245 _test.file = gitignore.File + "-invalid-with-errors" 246 _test.error = func(e gitignore.Error) bool { 247 _test.errors = append(_test.errors, e) 248 return true 249 } 250 _test.instance = func(path string) (gitignore.GitIgnore, error) { 251 return gitignore.NewRepositoryWithErrors( 252 path, _test.file, _test.error, 253 ), nil 254 } 255 256 // perform the invalid repository tests 257 invalid(t, _test) 258 } // TestInvalidRepositoryWithErrors() 259 260 func TestInvalidRepositoryWithErrorsFalse(t *testing.T) { 261 _test := &repositorytest{} 262 _test.file = gitignore.File + "-invalid-with-errors-false" 263 _test.error = func(e gitignore.Error) bool { 264 _test.errors = append(_test.errors, e) 265 return false 266 } 267 _test.instance = func(path string) (gitignore.GitIgnore, error) { 268 return gitignore.NewRepositoryWithErrors( 269 path, _test.file, _test.error, 270 ), nil 271 } 272 273 // perform the invalid repository tests 274 invalid(t, _test) 275 } // TestInvalidRepositoryWithErrorsFalse() 276 277 func TestInvalidRepositoryWithCache(t *testing.T) { 278 _test := &repositorytest{} 279 _test.file = gitignore.File + "-invalid-with-cache" 280 _test.cache = gitignore.NewCache() 281 _test.cached = true 282 _test.error = func(e gitignore.Error) bool { 283 _test.errors = append(_test.errors, e) 284 return true 285 } 286 _test.instance = func(path string) (gitignore.GitIgnore, error) { 287 return gitignore.NewRepositoryWithCache( 288 path, _test.file, _test.cache, _test.error, 289 ), nil 290 } 291 292 // perform the invalid repository tests 293 invalid(t, _test) 294 295 // repeat the tests using a default cache 296 _test.cache = nil 297 invalid(t, _test) 298 } // TestInvalidRepositoryWithCache() 299 300 // 301 // helper functions 302 // 303 304 func repository(t *testing.T, test *repositorytest, m []match) { 305 // if the test has no configured directory, then create a new 306 // directory with the required .gitignore files 307 if test.directory == "" { 308 // what name should we use for the .gitignore file? 309 // - if none is given, use the default 310 _file := test.file 311 if _file == "" { 312 _file = gitignore.File 313 } 314 315 // create a temporary directory populated with sample .gitignore files 316 // - first, augment the test data to include file names 317 _map := make(map[string]string) 318 for _k, _content := range _GITREPOSITORY { 319 _map[_k+"/"+_file] = _content 320 } 321 _dir, _err := dir(_map) 322 if _err != nil { 323 t.Fatalf("unable to create temporary directory: %s", _err.Error()) 324 } 325 test.directory = _dir 326 } 327 328 // create the repository 329 _repository, _err := test.create(test.directory, true) 330 if _err != nil { 331 t.Fatalf("unable to create repository: %s", _err.Error()) 332 } 333 334 // ensure we have a non-nill repository returned 335 if _repository == nil { 336 t.Error("expected non-nill GitIgnore repository instance; nil found") 337 } 338 339 // ensure the base of the repository is correct 340 if _repository.Base() != test.directory { 341 t.Errorf( 342 "repository.Base() mismatch; expected %q, got %q", 343 test.directory, _repository.Base(), 344 ) 345 } 346 347 // we need to check each test to see if it's matching against a 348 // GIT_DIR/info/exclude 349 // - we only do this if the target does not use .gitignore 350 // as the name of the ignore file 351 _prepare := func(m match) match { 352 if test.file == "" || test.file == gitignore.File { 353 return m 354 } else if m.Exclude { 355 return match{m.Path, "", false, m.Exclude} 356 } else { 357 return m 358 } 359 } // _prepare() 360 361 // perform the repository matching using absolute paths 362 _cb := func(path string, isdir bool) gitignore.Match { 363 _path := filepath.Join(_repository.Base(), path) 364 return _repository.Absolute(_path, isdir) 365 } 366 for _, _test := range m { 367 do(t, _cb, _prepare(_test)) 368 } 369 370 // repeat the tests using relative paths 371 _repository, _err = test.create(test.directory, true) 372 if _err != nil { 373 t.Fatalf("unable to create repository: %s", _err.Error()) 374 } 375 _cb = func(path string, isdir bool) gitignore.Match { 376 return _repository.Relative(path, isdir) 377 } 378 for _, _test := range m { 379 do(t, _cb, _prepare(_test)) 380 } 381 382 // perform absolute path tests with paths not under the same repository 383 _map := make(map[string]string) 384 for _, _test := range m { 385 _map[_test.Path] = " " 386 } 387 _new, _err := dir(_map) 388 if _err != nil { 389 t.Fatalf("unable to create temporary directory: %s", _err.Error()) 390 } 391 defer os.RemoveAll(_new) 392 393 // first, perform Match() tests 394 _repository, _err = test.create(test.directory, true) 395 if _err != nil { 396 t.Fatalf("unable to create repository: %s", _err.Error()) 397 } 398 for _, _test := range m { 399 _path := filepath.Join(_new, _test.Local()) 400 _match := _repository.Match(_path) 401 if _match != nil { 402 t.Fatalf("unexpected match; expected nil, got %v", _match) 403 } 404 } 405 406 // next, perform Absolute() tests 407 _repository, _err = test.create(test.directory, true) 408 if _err != nil { 409 t.Fatalf("unable to create repository: %s", _err.Error()) 410 } 411 for _, _test := range m { 412 // build the absolute path 413 _path := filepath.Join(_new, _test.Local()) 414 415 // we don't expect to match paths not under this repository 416 _match := _repository.Absolute(_path, _test.IsDir()) 417 if _match != nil { 418 t.Fatalf("unexpected match; expected nil, got %v", _match) 419 } 420 } 421 422 // now, repeat the Match() test after having first removed the 423 // temporary directory 424 // - we are testing correct handling of missing files 425 _err = os.RemoveAll(_new) 426 if _err != nil { 427 t.Fatalf( 428 "unable to remove temporary directory %s: %s", 429 _new, _err.Error(), 430 ) 431 } 432 _repository, _err = test.create(test.directory, true) 433 if _err != nil { 434 t.Fatalf("unable to create repository: %s", _err.Error()) 435 } 436 for _, _test := range m { 437 _path := filepath.Join(_new, _test.Local()) 438 439 // if we have an error handler configured, we should be recording 440 // and error in this call to Match() 441 _before := len(test.errors) 442 443 // perform the match 444 _match := _repository.Match(_path) 445 if _match != nil { 446 t.Fatalf("unexpected match; expected nil, got %v", _match) 447 } 448 449 // were we recording errors? 450 if test.error != nil { 451 _after := len(test.errors) 452 if !(_after > _before) { 453 t.Fatalf( 454 "expected Match() error; none found for %s", 455 _path, 456 ) 457 } 458 459 // ensure the most recent error is "not exists" 460 _latest := test.errors[_after-1] 461 _underlying := _latest.Underlying() 462 if !os.IsNotExist(_underlying) { 463 t.Fatalf( 464 "unexpected Match() error for %s; expected %q, got %q", 465 _path, os.ErrNotExist.Error(), _underlying.Error(), 466 ) 467 } 468 } 469 } 470 471 // ensure Match() behaves as expected if the absolute path cannot 472 // be determined 473 // - we do this by choosing as our working directory a path 474 // that this process does not have permission to 475 _dir, _err := dir(nil) 476 if _err != nil { 477 t.Fatalf("unable to create temporary directory: %s", _err.Error()) 478 } 479 defer os.RemoveAll(_dir) 480 481 _cwd, _err := os.Getwd() 482 if _err != nil { 483 t.Fatalf("unable to retrieve working directory: %s", _err.Error()) 484 } 485 _err = os.Chdir(_dir) 486 if _err != nil { 487 t.Fatalf("unable to chdir into temporary directory: %s", _err.Error()) 488 } 489 defer func(dir string) { _ = os.Chdir(dir) }(_cwd) 490 491 // remove permission from the temporary directory 492 _err = os.Chmod(_dir, 0) 493 if _err != nil { 494 t.Fatalf( 495 "unable to remove temporary directory %s: %s", 496 _dir, _err.Error(), 497 ) 498 } 499 500 // perform the repository tests 501 _repository, _err = test.create(test.directory, true) 502 if _err != nil { 503 t.Fatalf("unable to create repository: %s", _err.Error()) 504 } 505 for _, _test := range m { 506 _match := _repository.Match(_test.Local()) 507 if _match != nil { 508 t.Fatalf("unexpected match; expected nil, not %v", _match) 509 } 510 } 511 512 if test.errors != nil { 513 // ensure the number of errors is expected 514 if len(test.errors) != test.bad { 515 t.Fatalf( 516 "unexpected repository errors; expected %d, got %d", 517 test.bad, len(test.errors), 518 ) 519 } else { 520 // if we're here, then we intended to record errors 521 // - ensure we recorded the expected errors 522 for _i := 0; _i < len(test.errors); _i++ { 523 _got := test.errors[_i] 524 _underlying := _got.Underlying() 525 if os.IsNotExist(_underlying) || 526 os.IsPermission(_underlying) { 527 continue 528 } else { 529 t.Log(_i) 530 t.Fatalf("unexpected repository error: %s", _got.Error()) 531 } 532 } 533 } 534 } 535 } // repository() 536 537 func invalid(t *testing.T, test *repositorytest) { 538 // create a temporary file to use as the repository 539 _file, _err := file("") 540 if _err != nil { 541 t.Fatalf("unable to create temporary file: %s", _err.Error()) 542 } 543 defer os.Remove(_file.Name()) 544 545 // test repository instance creation against a file 546 _repository, _err := test.create(_file.Name(), false) 547 if _err == nil { 548 t.Errorf( 549 "invalid repository error; expected %q, got nil", 550 gitignore.InvalidDirectoryError.Error(), 551 ) 552 } else if _err != gitignore.InvalidDirectoryError { 553 t.Errorf( 554 "invalid repository mismatch; expected %q, got %q", 555 gitignore.InvalidDirectoryError.Error(), _err.Error(), 556 ) 557 } 558 559 // ensure no repository is returned 560 if _repository != nil { 561 t.Errorf( 562 "invalid repository; expected nil, got %v", 563 _repository, 564 ) 565 } 566 567 // now, remove the temporary file and repeat the tests 568 _err = os.Remove(_file.Name()) 569 if _err != nil { 570 t.Fatalf( 571 "unable to remove temporary file %s: %s", 572 _file.Name(), _err.Error(), 573 ) 574 } 575 576 // test repository instance creating against a missing file 577 _repository, _err = test.create(_file.Name(), false) 578 if _err == nil { 579 t.Errorf( 580 "invalid repository error; expected %q, got nil", 581 gitignore.InvalidDirectoryError.Error(), 582 ) 583 } else if !os.IsNotExist(_err) { 584 t.Errorf( 585 "invalid repository mismatch; "+ 586 "expected no such file or directory, got %q", 587 _err.Error(), 588 ) 589 } 590 591 // ensure no repository is returned 592 if _repository != nil { 593 t.Errorf( 594 "invalid repository; expected nil, got %v", 595 _repository, 596 ) 597 } 598 599 // ensure we can't create a repository instance where the absolute path 600 // of the repository cannot be determined 601 // - we do this by choosing a working directory this process does 602 // not have access to and using a relative path 603 _map := map[string]string{gitignore.File: _GITIGNORE} 604 _dir, _err := dir(_map) 605 if _err != nil { 606 t.Fatalf("unable to create a temporary directory: %s", _err.Error()) 607 } 608 defer os.RemoveAll(_dir) 609 610 // now change the working directory 611 _cwd, _err := os.Getwd() 612 if _err != nil { 613 t.Fatalf("unable to retrieve working directory: %s", _err.Error()) 614 } 615 _err = os.Chdir(_dir) 616 if _err != nil { 617 t.Fatalf("unable to chdir into temporary directory: %s", _err.Error()) 618 } 619 defer func(dir string) { _ = os.Chdir(dir) }(_cwd) 620 621 // remove permissions from the working directory 622 _err = os.Chmod(_dir, 0) 623 if _err != nil { 624 t.Fatalf("unable remove temporary directory permissions: %s: %s", 625 _dir, _err.Error(), 626 ) 627 } 628 629 // test repository instance creating against a relative path 630 // - the relative path exists 631 _, _err = test.create(gitignore.File, false) 632 if _err == nil { 633 t.Errorf("expected repository error, got nil") 634 } else if os.IsNotExist(_err) { 635 t.Errorf( 636 "unexpected repository error; file exists, but %q returned", 637 _err.Error(), 638 ) 639 } 640 641 // next, create a repository where we do not have read permission 642 // to a .gitignore file within the repository 643 // - this should trigger a panic() when attempting a file match 644 for _, _test := range _REPOSITORYMATCHES { 645 _map[_test.Path] = " " 646 } 647 _dir, _err = dir(_map) 648 if _err != nil { 649 t.Fatalf("unable to create a temporary directory: %s", _err.Error()) 650 } 651 defer os.RemoveAll(_dir) 652 653 _git := filepath.Join(_dir, gitignore.File) 654 _err = os.Chmod(_git, 0) 655 if _err != nil { 656 t.Fatalf("unable remove temporary .gitignore permissions: %s: %s", 657 _git, _err.Error(), 658 ) 659 } 660 661 // attempt to match a path in this repository 662 // - it can be anything, so we just use the .gitignore itself 663 // - between each test we recreate the repository instance to 664 // remove the effect of any caching 665 _instance := func() gitignore.GitIgnore { 666 // reset the cache 667 if test.cached { 668 if test.cache != nil { 669 test.cache = gitignore.NewCache() 670 } 671 } 672 673 // create the new repository 674 _repository, _err := test.create(_dir, false) 675 if _err != nil { 676 t.Fatalf("unable to create repository: %s", _err.Error()) 677 } 678 679 // return the repository 680 return _repository 681 } 682 for _, _match := range _REPOSITORYMATCHES { 683 _local := _match.Local() 684 _isdir := _match.IsDir() 685 _path := filepath.Join(_dir, _local) 686 687 // try Match() with an absolute path 688 _test := &invalidtest{repositorytest: test} 689 _test.tag = "Match()" 690 _test.match = func() gitignore.Match { 691 return _instance().Match(_path) 692 } 693 run(t, _test) 694 695 // try Absolute() with an absolute path 696 _test = &invalidtest{repositorytest: test} 697 _test.tag = "Absolute()" 698 _test.match = func() gitignore.Match { 699 return _instance().Absolute(_path, _isdir) 700 } 701 run(t, _test) 702 703 // try Absolute() with an absolute path 704 _test = &invalidtest{repositorytest: test} 705 _test.tag = "Relative()" 706 _test.match = func() gitignore.Match { 707 return _instance().Relative(_local, _isdir) 708 } 709 run(t, _test) 710 } 711 } // invalid() 712 713 func run(t *testing.T, test *invalidtest) { 714 // perform the match, and ensure it returns nil, nil 715 _match := test.match() 716 if _match != nil { 717 t.Fatalf("%s: unexpected match: %v", test.tag, _match) 718 } else if test.errors == nil { 719 return 720 } 721 722 // if we're here, then we intended to record errors 723 // - ensure we recorded the expected errors 724 for _i := 0; _i < len(test.errors); _i++ { 725 _got := test.errors[_i] 726 _underlying := _got.Underlying() 727 if os.IsNotExist(_underlying) || 728 os.IsPermission(_underlying) { 729 continue 730 } else { 731 t.Fatalf( 732 "%s: unexpected error: %q", 733 test.tag, _got.Error(), 734 ) 735 } 736 } 737 } // run()