sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/repoowners/repoowners_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package repoowners 18 19 import ( 20 "fmt" 21 "os" 22 "path/filepath" 23 "reflect" 24 "regexp" 25 "sync" 26 "testing" 27 28 "github.com/sirupsen/logrus" 29 30 "k8s.io/apimachinery/pkg/util/diff" 31 "k8s.io/apimachinery/pkg/util/sets" 32 prowConf "sigs.k8s.io/prow/pkg/config" 33 "sigs.k8s.io/prow/pkg/git/localgit" 34 "sigs.k8s.io/prow/pkg/github" 35 "sigs.k8s.io/prow/pkg/plugins/ownersconfig" 36 ) 37 38 var ( 39 defaultBranch = "master" // TODO(fejta): localgit.DefaultBranch() 40 testFiles = map[string][]byte{ 41 "foo": []byte(`approvers: 42 - bob`), 43 "OWNERS": []byte(`approvers: 44 - cjwagner 45 reviewers: 46 - Alice 47 - bob 48 required_reviewers: 49 - chris 50 labels: 51 - EVERYTHING`), 52 "src/OWNERS": []byte(`approvers: 53 - Best-Approvers`), 54 "src/dir/OWNERS": []byte(`approvers: 55 - bob 56 reviewers: 57 - alice 58 - "@CJWagner" 59 - jakub 60 required_reviewers: 61 - ben 62 labels: 63 - src-code`), 64 "src/dir/subdir/OWNERS": []byte(`approvers: 65 - bob 66 - alice 67 reviewers: 68 - bob 69 - alice`), 70 "src/dir/conformance/OWNERS": []byte(`options: 71 no_parent_owners: true 72 auto_approve_unowned_subfolders: true 73 approvers: 74 - mml`), 75 "docs/file.md": []byte(`--- 76 approvers: 77 - ALICE 78 79 labels: 80 - docs 81 ---`), 82 "vendor/OWNERS": []byte(`approvers: 83 - alice`), 84 "vendor/k8s.io/client-go/OWNERS": []byte(`approvers: 85 - bob`), 86 } 87 88 testFilesRe = map[string][]byte{ 89 // regexp filtered 90 "re/OWNERS": []byte(`filters: 91 ".*": 92 labels: 93 - re/all 94 "\\.go$": 95 labels: 96 - re/go`), 97 "re/a/OWNERS": []byte(`filters: 98 "\\.md$": 99 labels: 100 - re/md-in-a 101 "\\.go$": 102 labels: 103 - re/go-in-a`), 104 } 105 ) 106 107 // regexpAll is used to construct a default {regexp -> values} mapping for ".*" 108 func regexpAll(values ...string) map[*regexp.Regexp]sets.Set[string] { 109 return map[*regexp.Regexp]sets.Set[string]{nil: sets.New[string](values...)} 110 } 111 112 // patternAll is used to construct a default {regexp string -> values} mapping for ".*" 113 func patternAll(values ...string) map[string]sets.Set[string] { 114 // use "" to represent nil and distinguish it from a ".*" regexp (which shouldn't exist). 115 return map[string]sets.Set[string]{"": sets.New[string](values...)} 116 } 117 118 type cacheOptions struct { 119 hasAliases bool 120 121 mdYaml bool 122 commonFileChanged bool 123 mdFileChanged bool 124 ownersAliasesFileChanged bool 125 ownersFileChanged bool 126 } 127 128 type fakeGitHubClient struct { 129 Collaborators []string 130 ref string 131 } 132 133 func (f *fakeGitHubClient) ListCollaborators(org, repo string) ([]github.User, error) { 134 result := make([]github.User, 0, len(f.Collaborators)) 135 for _, login := range f.Collaborators { 136 result = append(result, github.User{Login: login}) 137 } 138 return result, nil 139 } 140 141 func (f *fakeGitHubClient) GetRef(org, repo, ref string) (string, error) { 142 return f.ref, nil 143 } 144 145 func getTestClient( 146 files map[string][]byte, 147 enableMdYaml, 148 skipCollab, 149 includeAliases bool, 150 ignorePreconfiguredDefaults bool, 151 ownersDirDenylistDefault []string, 152 ownersDirDenylistByRepo map[string][]string, 153 extraBranchesAndFiles map[string]map[string][]byte, 154 cacheOptions *cacheOptions, 155 clients localgit.Clients, 156 ) (*Client, func(), error) { 157 testAliasesFile := map[string][]byte{ 158 "OWNERS_ALIASES": []byte("aliases:\n Best-approvers:\n - carl\n - cjwagner\n best-reviewers:\n - Carl\n - BOB"), 159 } 160 161 localGit, git, err := clients() 162 if err != nil { 163 return nil, nil, err 164 } 165 166 if localgit.DefaultBranch("") != defaultBranch { 167 localGit.InitialBranch = defaultBranch 168 } 169 170 if err := localGit.MakeFakeRepo("org", "repo"); err != nil { 171 return nil, nil, fmt.Errorf("cannot make fake repo: %w", err) 172 } 173 174 if err := localGit.AddCommit("org", "repo", files); err != nil { 175 return nil, nil, fmt.Errorf("cannot add initial commit: %w", err) 176 } 177 if includeAliases { 178 if err := localGit.AddCommit("org", "repo", testAliasesFile); err != nil { 179 return nil, nil, fmt.Errorf("cannot add OWNERS_ALIASES commit: %w", err) 180 } 181 } 182 if len(extraBranchesAndFiles) > 0 { 183 for branch, extraFiles := range extraBranchesAndFiles { 184 if err := localGit.CheckoutNewBranch("org", "repo", branch); err != nil { 185 return nil, nil, err 186 } 187 if len(extraFiles) > 0 { 188 if err := localGit.AddCommit("org", "repo", extraFiles); err != nil { 189 return nil, nil, fmt.Errorf("cannot add commit: %w", err) 190 } 191 } 192 } 193 if err := localGit.Checkout("org", "repo", defaultBranch); err != nil { 194 return nil, nil, err 195 } 196 } 197 cache := newCache() 198 if cacheOptions != nil { 199 var entry cacheEntry 200 entry.sha, err = localGit.RevParse("org", "repo", "HEAD") 201 if err != nil { 202 return nil, nil, fmt.Errorf("cannot get commit SHA: %w", err) 203 } 204 if cacheOptions.hasAliases { 205 entry.aliases = make(map[string]sets.Set[string]) 206 } 207 entry.owners = &RepoOwners{ 208 enableMDYAML: cacheOptions.mdYaml, 209 } 210 if cacheOptions.commonFileChanged { 211 md := map[string][]byte{"common": []byte(`--- 212 This file could be anything 213 ---`)} 214 if err := localGit.AddCommit("org", "repo", md); err != nil { 215 return nil, nil, fmt.Errorf("cannot add commit: %w", err) 216 } 217 } 218 if cacheOptions.mdFileChanged { 219 md := map[string][]byte{"docs/file.md": []byte(`--- 220 approvers: 221 - ALICE 222 223 224 labels: 225 - docs 226 ---`)} 227 if err := localGit.AddCommit("org", "repo", md); err != nil { 228 return nil, nil, fmt.Errorf("cannot add commit: %w", err) 229 } 230 } 231 if cacheOptions.ownersAliasesFileChanged { 232 testAliasesFile = map[string][]byte{ 233 "OWNERS_ALIASES": []byte("aliases:\n Best-approvers:\n\n - carl\n - cjwagner\n best-reviewers:\n - Carl\n - BOB"), 234 } 235 if err := localGit.AddCommit("org", "repo", testAliasesFile); err != nil { 236 return nil, nil, fmt.Errorf("cannot add commit: %w", err) 237 } 238 } 239 if cacheOptions.ownersFileChanged { 240 owners := map[string][]byte{ 241 "OWNERS": []byte(`approvers: 242 - cjwagner 243 reviewers: 244 - "@Alice" 245 - bob 246 247 required_reviewers: 248 - chris 249 labels: 250 - EVERYTHING`), 251 } 252 if err := localGit.AddCommit("org", "repo", owners); err != nil { 253 return nil, nil, fmt.Errorf("cannot add commit: %w", err) 254 } 255 } 256 cache.data["org"+"/"+"repo:master"] = entry 257 // mark this entry is cache 258 entry.owners.baseDir = "cache" 259 } 260 ghc := &fakeGitHubClient{Collaborators: []string{"cjwagner", "k8s-ci-robot", "alice", "bob", "carl", "mml", "maggie"}} 261 ghc.ref, err = localGit.RevParse("org", "repo", "HEAD") 262 if err != nil { 263 return nil, nil, fmt.Errorf("cannot get commit SHA: %w", err) 264 } 265 return &Client{ 266 logger: logrus.WithField("client", "repoowners"), 267 ghc: ghc, 268 delegate: &delegate{ 269 git: git, 270 cache: cache, 271 272 mdYAMLEnabled: func(org, repo string) bool { 273 return enableMdYaml 274 }, 275 skipCollaborators: func(org, repo string) bool { 276 return skipCollab 277 }, 278 ownersDirDenylist: func() *prowConf.OwnersDirDenylist { 279 return &prowConf.OwnersDirDenylist{ 280 Repos: ownersDirDenylistByRepo, 281 Default: ownersDirDenylistDefault, 282 IgnorePreconfiguredDefaults: ignorePreconfiguredDefaults, 283 } 284 }, 285 filenames: ownersconfig.FakeResolver, 286 }, 287 }, 288 // Clean up function 289 func() { 290 git.Clean() 291 localGit.Clean() 292 }, 293 nil 294 } 295 296 func TestOwnersDirDenylistV2(t *testing.T) { 297 testOwnersDirDenylist(localgit.NewV2, t) 298 } 299 300 func testOwnersDirDenylist(clients localgit.Clients, t *testing.T) { 301 getRepoOwnersWithDenylist := func(t *testing.T, defaults []string, byRepo map[string][]string, ignorePreconfiguredDefaults bool) *RepoOwners { 302 client, cleanup, err := getTestClient(testFiles, true, false, true, ignorePreconfiguredDefaults, defaults, byRepo, nil, nil, clients) 303 if err != nil { 304 t.Fatalf("Error creating test client: %v.", err) 305 } 306 defer cleanup() 307 308 ro, err := client.LoadRepoOwners("org", "repo", defaultBranch) 309 if err != nil { 310 t.Fatalf("Unexpected error loading RepoOwners: %v.", err) 311 } 312 313 return ro.(*RepoOwners) 314 } 315 316 type testConf struct { 317 denylistDefault []string 318 denylistByRepo map[string][]string 319 ignorePreconfiguredDefaults bool 320 includeDirs []string 321 excludeDirs []string 322 } 323 324 tests := map[string]testConf{} 325 326 tests["denylist by org"] = testConf{ 327 denylistByRepo: map[string][]string{ 328 "org": {"src"}, 329 }, 330 includeDirs: []string{""}, 331 excludeDirs: []string{"src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 332 } 333 tests["denylist by org/repo"] = testConf{ 334 denylistByRepo: map[string][]string{ 335 "org/repo": {"src"}, 336 }, 337 includeDirs: []string{""}, 338 excludeDirs: []string{"src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 339 } 340 tests["denylist by default"] = testConf{ 341 denylistDefault: []string{"src"}, 342 includeDirs: []string{""}, 343 excludeDirs: []string{"src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 344 } 345 tests["subdir denylist"] = testConf{ 346 denylistDefault: []string{"dir"}, 347 includeDirs: []string{"", "src"}, 348 excludeDirs: []string{"src/dir", "src/dir/conformance", "src/dir/subdir"}, 349 } 350 tests["no denylist setup"] = testConf{ 351 includeDirs: []string{"", "src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 352 } 353 tests["denylist setup but not matching this repo"] = testConf{ 354 denylistByRepo: map[string][]string{ 355 "not_org/not_repo": {"src"}, 356 "not_org": {"src"}, 357 }, 358 includeDirs: []string{"", "src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 359 } 360 tests["non-matching denylist"] = testConf{ 361 denylistDefault: []string{"sr$"}, 362 includeDirs: []string{"", "src", "src/dir", "src/dir/conformance", "src/dir/subdir"}, 363 } 364 tests["path denylist"] = testConf{ 365 denylistDefault: []string{"src/dir"}, 366 includeDirs: []string{"", "src"}, 367 excludeDirs: []string{"src/dir", "src/dir/conformance", "src/dir/subdir"}, 368 } 369 tests["regexp denylist path"] = testConf{ 370 denylistDefault: []string{"src/dir/."}, 371 includeDirs: []string{"", "src", "src/dir"}, 372 excludeDirs: []string{"src/dir/conformance", "src/dir/subdir"}, 373 } 374 tests["path substring"] = testConf{ 375 denylistDefault: []string{"/c"}, 376 includeDirs: []string{"", "src", "src/dir", "src/dir/subdir"}, 377 excludeDirs: []string{"src/dir/conformance"}, 378 } 379 tests["exclude preconfigured defaults"] = testConf{ 380 includeDirs: []string{"", "src", "src/dir", "src/dir/subdir", "vendor"}, 381 excludeDirs: []string{"vendor/k8s.io/client-go"}, 382 } 383 tests["ignore preconfigured defaults"] = testConf{ 384 includeDirs: []string{"", "src", "src/dir", "src/dir/subdir", "vendor", "vendor/k8s.io/client-go"}, 385 ignorePreconfiguredDefaults: true, 386 } 387 388 for name, conf := range tests { 389 t.Run(name, func(t *testing.T) { 390 ro := getRepoOwnersWithDenylist(t, conf.denylistDefault, conf.denylistByRepo, conf.ignorePreconfiguredDefaults) 391 392 includeDirs := sets.New[string](conf.includeDirs...) 393 excludeDirs := sets.New[string](conf.excludeDirs...) 394 for dir := range ro.approvers { 395 if excludeDirs.Has(dir) { 396 t.Errorf("Expected directory %s to be excluded from the approvers map", dir) 397 } 398 includeDirs.Delete(dir) 399 } 400 for dir := range ro.reviewers { 401 if excludeDirs.Has(dir) { 402 t.Errorf("Expected directory %s to be excluded from the reviewers map", dir) 403 } 404 includeDirs.Delete(dir) 405 } 406 407 for _, dir := range sets.List(includeDirs) { 408 t.Errorf("Expected to find approvers or reviewers for directory %s", dir) 409 } 410 }) 411 } 412 } 413 414 func TestOwnersRegexpFilteringV2(t *testing.T) { 415 testOwnersRegexpFiltering(localgit.NewV2, t) 416 } 417 418 func testOwnersRegexpFiltering(clients localgit.Clients, t *testing.T) { 419 tests := map[string]sets.Set[string]{ 420 "re/a/go.go": sets.New[string]("re/all", "re/go", "re/go-in-a"), 421 "re/a/md.md": sets.New[string]("re/all", "re/md-in-a"), 422 "re/a/txt.txt": sets.New[string]("re/all"), 423 "re/go.go": sets.New[string]("re/all", "re/go"), 424 "re/txt.txt": sets.New[string]("re/all"), 425 "re/b/md.md": sets.New[string]("re/all"), 426 } 427 428 client, cleanup, err := getTestClient(testFilesRe, true, false, true, false, nil, nil, nil, nil, clients) 429 if err != nil { 430 t.Fatalf("Error creating test client: %v.", err) 431 } 432 defer cleanup() 433 434 r, err := client.LoadRepoOwners("org", "repo", defaultBranch) 435 if err != nil { 436 t.Fatalf("Unexpected error loading RepoOwners: %v.", err) 437 } 438 ro := r.(*RepoOwners) 439 t.Logf("labels: %#v\n\n", ro.labels) 440 for file, expected := range tests { 441 if got := ro.FindLabelsForFile(file); !got.Equal(expected) { 442 t.Errorf("For file %q expected labels %q, but got %q.", file, sets.List(expected), sets.List(got)) 443 } 444 } 445 } 446 447 func strP(str string) *string { 448 return &str 449 } 450 451 func TestLoadRepoOwnersV2(t *testing.T) { 452 testLoadRepoOwners(localgit.NewV2, t) 453 } 454 455 func testLoadRepoOwners(clients localgit.Clients, t *testing.T) { 456 t.Parallel() 457 tests := []struct { 458 name string 459 mdEnabled bool 460 aliasesFileExists bool 461 skipCollaborators bool 462 // used for testing OWNERS from a branch different from master 463 branch *string 464 extraBranchesAndFiles map[string]map[string][]byte 465 466 expectedApprovers, expectedReviewers, expectedRequiredReviewers, expectedLabels map[string]map[string]sets.Set[string] 467 468 expectedOptions map[string]dirOptions 469 cacheOptions *cacheOptions 470 expectedReusable bool 471 }{ 472 { 473 name: "no alias, no md", 474 expectedApprovers: map[string]map[string]sets.Set[string]{ 475 "": patternAll("cjwagner"), 476 "src": patternAll(), 477 "src/dir": patternAll("bob"), 478 "src/dir/conformance": patternAll("mml"), 479 "src/dir/subdir": patternAll("alice", "bob"), 480 "vendor": patternAll("alice"), 481 }, 482 expectedReviewers: map[string]map[string]sets.Set[string]{ 483 "": patternAll("alice", "bob"), 484 "src/dir": patternAll("alice", "cjwagner"), 485 "src/dir/subdir": patternAll("alice", "bob"), 486 }, 487 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 488 "": patternAll("chris"), 489 "src/dir": patternAll("ben"), 490 }, 491 expectedLabels: map[string]map[string]sets.Set[string]{ 492 "": patternAll("EVERYTHING"), 493 "src/dir": patternAll("src-code"), 494 }, 495 expectedOptions: map[string]dirOptions{ 496 "src/dir/conformance": { 497 NoParentOwners: true, 498 AutoApproveUnownedSubfolders: true, 499 }, 500 }, 501 }, 502 { 503 name: "alias, no md", 504 aliasesFileExists: true, 505 expectedApprovers: map[string]map[string]sets.Set[string]{ 506 "": patternAll("cjwagner"), 507 "src": patternAll("carl", "cjwagner"), 508 "src/dir": patternAll("bob"), 509 "src/dir/conformance": patternAll("mml"), 510 "src/dir/subdir": patternAll("alice", "bob"), 511 "vendor": patternAll("alice"), 512 }, 513 expectedReviewers: map[string]map[string]sets.Set[string]{ 514 "": patternAll("alice", "bob"), 515 "src/dir": patternAll("alice", "cjwagner"), 516 "src/dir/subdir": patternAll("alice", "bob"), 517 }, 518 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 519 "": patternAll("chris"), 520 "src/dir": patternAll("ben"), 521 }, 522 expectedLabels: map[string]map[string]sets.Set[string]{ 523 "": patternAll("EVERYTHING"), 524 "src/dir": patternAll("src-code"), 525 }, 526 expectedOptions: map[string]dirOptions{ 527 "src/dir/conformance": { 528 NoParentOwners: true, 529 AutoApproveUnownedSubfolders: true, 530 }, 531 }, 532 }, 533 { 534 name: "alias, md", 535 aliasesFileExists: true, 536 mdEnabled: true, 537 expectedApprovers: map[string]map[string]sets.Set[string]{ 538 "": patternAll("cjwagner"), 539 "src": patternAll("carl", "cjwagner"), 540 "src/dir": patternAll("bob"), 541 "src/dir/conformance": patternAll("mml"), 542 "src/dir/subdir": patternAll("alice", "bob"), 543 "docs/file.md": patternAll("alice"), 544 "vendor": patternAll("alice"), 545 }, 546 expectedReviewers: map[string]map[string]sets.Set[string]{ 547 "": patternAll("alice", "bob"), 548 "src/dir": patternAll("alice", "cjwagner"), 549 "src/dir/subdir": patternAll("alice", "bob"), 550 }, 551 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 552 "": patternAll("chris"), 553 "src/dir": patternAll("ben"), 554 }, 555 expectedLabels: map[string]map[string]sets.Set[string]{ 556 "": patternAll("EVERYTHING"), 557 "src/dir": patternAll("src-code"), 558 "docs/file.md": patternAll("docs"), 559 }, 560 expectedOptions: map[string]dirOptions{ 561 "src/dir/conformance": { 562 NoParentOwners: true, 563 AutoApproveUnownedSubfolders: true, 564 }, 565 }, 566 }, 567 { 568 name: "OWNERS from non-default branch", 569 branch: strP("release-1.10"), 570 extraBranchesAndFiles: map[string]map[string][]byte{ 571 "release-1.10": { 572 "src/doc/OWNERS": []byte("approvers:\n - maggie\n"), 573 }, 574 }, 575 expectedApprovers: map[string]map[string]sets.Set[string]{ 576 "": patternAll("cjwagner"), 577 "src": patternAll(), 578 "src/dir": patternAll("bob"), 579 "src/dir/conformance": patternAll("mml"), 580 "src/dir/subdir": patternAll("alice", "bob"), 581 "src/doc": patternAll("maggie"), 582 "vendor": patternAll("alice"), 583 }, 584 expectedReviewers: map[string]map[string]sets.Set[string]{ 585 "": patternAll("alice", "bob"), 586 "src/dir": patternAll("alice", "cjwagner"), 587 "src/dir/subdir": patternAll("alice", "bob"), 588 }, 589 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 590 "": patternAll("chris"), 591 "src/dir": patternAll("ben"), 592 }, 593 expectedLabels: map[string]map[string]sets.Set[string]{ 594 "": patternAll("EVERYTHING"), 595 "src/dir": patternAll("src-code"), 596 }, 597 expectedOptions: map[string]dirOptions{ 598 "src/dir/conformance": { 599 NoParentOwners: true, 600 AutoApproveUnownedSubfolders: true, 601 }, 602 }, 603 }, 604 { 605 name: "OWNERS from master branch while release branch diverges", 606 branch: strP(defaultBranch), 607 extraBranchesAndFiles: map[string]map[string][]byte{ 608 "release-1.10": { 609 "src/doc/OWNERS": []byte("approvers:\n - maggie\n"), 610 }, 611 }, 612 expectedApprovers: map[string]map[string]sets.Set[string]{ 613 "": patternAll("cjwagner"), 614 "src": patternAll(), 615 "src/dir": patternAll("bob"), 616 "src/dir/conformance": patternAll("mml"), 617 "src/dir/subdir": patternAll("alice", "bob"), 618 "vendor": patternAll("alice"), 619 }, 620 expectedReviewers: map[string]map[string]sets.Set[string]{ 621 "": patternAll("alice", "bob"), 622 "src/dir": patternAll("alice", "cjwagner"), 623 "src/dir/subdir": patternAll("alice", "bob"), 624 }, 625 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 626 "": patternAll("chris"), 627 "src/dir": patternAll("ben"), 628 }, 629 expectedLabels: map[string]map[string]sets.Set[string]{ 630 "": patternAll("EVERYTHING"), 631 "src/dir": patternAll("src-code"), 632 }, 633 expectedOptions: map[string]dirOptions{ 634 "src/dir/conformance": { 635 NoParentOwners: true, 636 AutoApproveUnownedSubfolders: true, 637 }, 638 }, 639 }, 640 { 641 name: "Skip collaborator checks, use only OWNERS files", 642 skipCollaborators: true, 643 expectedApprovers: map[string]map[string]sets.Set[string]{ 644 "": patternAll("cjwagner"), 645 "src": patternAll("best-approvers"), 646 "src/dir": patternAll("bob"), 647 "src/dir/conformance": patternAll("mml"), 648 "src/dir/subdir": patternAll("alice", "bob"), 649 "vendor": patternAll("alice"), 650 }, 651 expectedReviewers: map[string]map[string]sets.Set[string]{ 652 "": patternAll("alice", "bob"), 653 "src/dir": patternAll("alice", "cjwagner", "jakub"), 654 "src/dir/subdir": patternAll("alice", "bob"), 655 }, 656 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 657 "": patternAll("chris"), 658 "src/dir": patternAll("ben"), 659 }, 660 expectedLabels: map[string]map[string]sets.Set[string]{ 661 "": patternAll("EVERYTHING"), 662 "src/dir": patternAll("src-code"), 663 }, 664 expectedOptions: map[string]dirOptions{ 665 "src/dir/conformance": { 666 NoParentOwners: true, 667 AutoApproveUnownedSubfolders: true, 668 }, 669 }, 670 }, 671 { 672 name: "cache reuses, base sha equals to cache sha", 673 skipCollaborators: true, 674 cacheOptions: &cacheOptions{ 675 hasAliases: true, 676 }, 677 expectedReusable: true, 678 }, 679 { 680 name: "cache reuses, only change common files", 681 skipCollaborators: true, 682 cacheOptions: &cacheOptions{ 683 hasAliases: true, 684 commonFileChanged: true, 685 }, 686 expectedReusable: true, 687 }, 688 { 689 name: "cache does not reuse, mdYaml changed", 690 aliasesFileExists: true, 691 mdEnabled: true, 692 expectedApprovers: map[string]map[string]sets.Set[string]{ 693 "": patternAll("cjwagner"), 694 "src": patternAll("carl", "cjwagner"), 695 "src/dir": patternAll("bob"), 696 "src/dir/conformance": patternAll("mml"), 697 "src/dir/subdir": patternAll("alice", "bob"), 698 "docs/file.md": patternAll("alice"), 699 "vendor": patternAll("alice"), 700 }, 701 expectedReviewers: map[string]map[string]sets.Set[string]{ 702 "": patternAll("alice", "bob"), 703 "src/dir": patternAll("alice", "cjwagner"), 704 "src/dir/subdir": patternAll("alice", "bob"), 705 }, 706 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 707 "": patternAll("chris"), 708 "src/dir": patternAll("ben"), 709 }, 710 expectedLabels: map[string]map[string]sets.Set[string]{ 711 "": patternAll("EVERYTHING"), 712 "src/dir": patternAll("src-code"), 713 "docs/file.md": patternAll("docs"), 714 }, 715 expectedOptions: map[string]dirOptions{ 716 "src/dir/conformance": { 717 NoParentOwners: true, 718 AutoApproveUnownedSubfolders: true, 719 }, 720 }, 721 cacheOptions: &cacheOptions{}, 722 }, 723 { 724 name: "cache does not reuse, aliases is nil", 725 aliasesFileExists: true, 726 mdEnabled: true, 727 expectedApprovers: map[string]map[string]sets.Set[string]{ 728 "": patternAll("cjwagner"), 729 "src": patternAll("carl", "cjwagner"), 730 "src/dir": patternAll("bob"), 731 "src/dir/conformance": patternAll("mml"), 732 "src/dir/subdir": patternAll("alice", "bob"), 733 "docs/file.md": patternAll("alice"), 734 "vendor": patternAll("alice"), 735 }, 736 expectedReviewers: map[string]map[string]sets.Set[string]{ 737 "": patternAll("alice", "bob"), 738 "src/dir": patternAll("alice", "cjwagner"), 739 "src/dir/subdir": patternAll("alice", "bob"), 740 }, 741 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 742 "": patternAll("chris"), 743 "src/dir": patternAll("ben"), 744 }, 745 expectedLabels: map[string]map[string]sets.Set[string]{ 746 "": patternAll("EVERYTHING"), 747 "src/dir": patternAll("src-code"), 748 "docs/file.md": patternAll("docs"), 749 }, 750 expectedOptions: map[string]dirOptions{ 751 "src/dir/conformance": { 752 NoParentOwners: true, 753 AutoApproveUnownedSubfolders: true, 754 }, 755 }, 756 cacheOptions: &cacheOptions{ 757 commonFileChanged: true, 758 }, 759 }, 760 { 761 name: "cache does not reuse, changes files contains OWNERS", 762 aliasesFileExists: true, 763 expectedApprovers: map[string]map[string]sets.Set[string]{ 764 "": patternAll("cjwagner"), 765 "src": patternAll("carl", "cjwagner"), 766 "src/dir": patternAll("bob"), 767 "src/dir/conformance": patternAll("mml"), 768 "src/dir/subdir": patternAll("alice", "bob"), 769 "vendor": patternAll("alice"), 770 }, 771 expectedReviewers: map[string]map[string]sets.Set[string]{ 772 "": patternAll("alice", "bob"), 773 "src/dir": patternAll("alice", "cjwagner"), 774 "src/dir/subdir": patternAll("alice", "bob"), 775 }, 776 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 777 "": patternAll("chris"), 778 "src/dir": patternAll("ben"), 779 }, 780 expectedLabels: map[string]map[string]sets.Set[string]{ 781 "": patternAll("EVERYTHING"), 782 "src/dir": patternAll("src-code"), 783 }, 784 expectedOptions: map[string]dirOptions{ 785 "src/dir/conformance": { 786 NoParentOwners: true, 787 AutoApproveUnownedSubfolders: true, 788 }, 789 }, 790 cacheOptions: &cacheOptions{ 791 hasAliases: true, 792 ownersFileChanged: true, 793 }, 794 }, 795 { 796 name: "cache does not reuse, changes files contains OWNERS_ALIASES", 797 aliasesFileExists: true, 798 expectedApprovers: map[string]map[string]sets.Set[string]{ 799 "": patternAll("cjwagner"), 800 "src": patternAll("carl", "cjwagner"), 801 "src/dir": patternAll("bob"), 802 "src/dir/conformance": patternAll("mml"), 803 "src/dir/subdir": patternAll("alice", "bob"), 804 "vendor": patternAll("alice"), 805 }, 806 expectedReviewers: map[string]map[string]sets.Set[string]{ 807 "": patternAll("alice", "bob"), 808 "src/dir": patternAll("alice", "cjwagner"), 809 "src/dir/subdir": patternAll("alice", "bob"), 810 }, 811 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 812 "": patternAll("chris"), 813 "src/dir": patternAll("ben"), 814 }, 815 expectedLabels: map[string]map[string]sets.Set[string]{ 816 "": patternAll("EVERYTHING"), 817 "src/dir": patternAll("src-code"), 818 }, 819 expectedOptions: map[string]dirOptions{ 820 "src/dir/conformance": { 821 NoParentOwners: true, 822 AutoApproveUnownedSubfolders: true, 823 }, 824 }, 825 cacheOptions: &cacheOptions{ 826 hasAliases: true, 827 ownersAliasesFileChanged: true, 828 }, 829 }, 830 { 831 name: "cache reuses, changes files contains .md, but mdYaml is false", 832 skipCollaborators: true, 833 cacheOptions: &cacheOptions{ 834 hasAliases: true, 835 mdFileChanged: true, 836 }, 837 expectedReusable: true, 838 }, 839 { 840 name: "cache does not reuse, changes files contains .md, and mdYaml is true", 841 aliasesFileExists: true, 842 mdEnabled: true, 843 expectedApprovers: map[string]map[string]sets.Set[string]{ 844 "": patternAll("cjwagner"), 845 "src": patternAll("carl", "cjwagner"), 846 "src/dir": patternAll("bob"), 847 "src/dir/conformance": patternAll("mml"), 848 "src/dir/subdir": patternAll("alice", "bob"), 849 "docs/file.md": patternAll("alice"), 850 "vendor": patternAll("alice"), 851 }, 852 expectedReviewers: map[string]map[string]sets.Set[string]{ 853 "": patternAll("alice", "bob"), 854 "src/dir": patternAll("alice", "cjwagner"), 855 "src/dir/subdir": patternAll("alice", "bob"), 856 }, 857 expectedRequiredReviewers: map[string]map[string]sets.Set[string]{ 858 "": patternAll("chris"), 859 "src/dir": patternAll("ben"), 860 }, 861 expectedLabels: map[string]map[string]sets.Set[string]{ 862 "": patternAll("EVERYTHING"), 863 "src/dir": patternAll("src-code"), 864 "docs/file.md": patternAll("docs"), 865 }, 866 expectedOptions: map[string]dirOptions{ 867 "src/dir/conformance": { 868 NoParentOwners: true, 869 AutoApproveUnownedSubfolders: true, 870 }, 871 }, 872 cacheOptions: &cacheOptions{ 873 hasAliases: true, 874 mdYaml: true, 875 mdFileChanged: true, 876 }, 877 }, 878 } 879 880 for i := range tests { 881 test := tests[i] 882 t.Run(test.name, func(t *testing.T) { 883 t.Parallel() 884 t.Logf("Running scenario %q", test.name) 885 client, cleanup, err := getTestClient(testFiles, test.mdEnabled, test.skipCollaborators, test.aliasesFileExists, false, nil, nil, test.extraBranchesAndFiles, test.cacheOptions, clients) 886 if err != nil { 887 t.Fatalf("Error creating test client: %v.", err) 888 } 889 t.Cleanup(cleanup) 890 891 base := defaultBranch 892 defer cleanup() 893 894 if test.branch != nil { 895 base = *test.branch 896 } 897 r, err := client.LoadRepoOwners("org", "repo", base) 898 if err != nil { 899 t.Fatalf("Unexpected error loading RepoOwners: %v.", err) 900 } 901 ro := r.(*RepoOwners) 902 if test.expectedReusable { 903 if ro.baseDir != "cache" { 904 t.Fatalf("expected cache must be reused, but got baseDir %q", ro.baseDir) 905 } 906 return 907 } else { 908 if ro.baseDir == "cache" { 909 t.Fatal("expected cache should not be reused, but reused") 910 } 911 } 912 if ro.baseDir == "" { 913 t.Fatal("Expected 'baseDir' to be populated.") 914 } 915 if (ro.RepoAliases != nil) != test.aliasesFileExists { 916 t.Fatalf("Expected 'RepoAliases' to be poplulated: %t, but got %t.", test.aliasesFileExists, ro.RepoAliases != nil) 917 } 918 if ro.enableMDYAML != test.mdEnabled { 919 t.Fatalf("Expected 'enableMdYaml' to be: %t, but got %t.", test.mdEnabled, ro.enableMDYAML) 920 } 921 922 check := func(field string, expected map[string]map[string]sets.Set[string], got map[string]map[*regexp.Regexp]sets.Set[string]) { 923 converted := map[string]map[string]sets.Set[string]{} 924 for path, m := range got { 925 converted[path] = map[string]sets.Set[string]{} 926 for re, s := range m { 927 var pattern string 928 if re != nil { 929 pattern = re.String() 930 } 931 converted[path][pattern] = s 932 } 933 } 934 if !reflect.DeepEqual(expected, converted) { 935 t.Errorf("Expected %s to be:\n%+v\ngot:\n%+v.", field, expected, converted) 936 } 937 } 938 check("approvers", test.expectedApprovers, ro.approvers) 939 check("reviewers", test.expectedReviewers, ro.reviewers) 940 check("required_reviewers", test.expectedRequiredReviewers, ro.requiredReviewers) 941 check("labels", test.expectedLabels, ro.labels) 942 if !reflect.DeepEqual(test.expectedOptions, ro.options) { 943 t.Errorf("Expected options to be:\n%#v\ngot:\n%#v.", test.expectedOptions, ro.options) 944 } 945 }) 946 } 947 } 948 949 const ( 950 baseDir = "" 951 leafDir = "a/b/c" 952 leafFilterDir = "a/b/e" 953 noParentsDir = "d" 954 noParentsFilterDir = "f" 955 nonExistentDir = "DELETED_DIR" 956 ) 957 958 var ( 959 mdFileReg = regexp.MustCompile(`.*\.md`) 960 txtFileReg = regexp.MustCompile(`.*\.txt`) 961 ) 962 963 func TestGetApprovers(t *testing.T) { 964 ro := &RepoOwners{ 965 approvers: map[string]map[*regexp.Regexp]sets.Set[string]{ 966 baseDir: regexpAll("alice", "bob"), 967 leafDir: regexpAll("carl", "dave"), 968 leafFilterDir: { 969 mdFileReg: sets.New[string]("carl", "dave"), 970 txtFileReg: sets.New[string]("elic"), 971 }, 972 noParentsDir: regexpAll("mml"), 973 noParentsFilterDir: { 974 mdFileReg: sets.New[string]("carl", "dave"), 975 txtFileReg: sets.New[string]("flex"), 976 }, 977 }, 978 options: map[string]dirOptions{ 979 noParentsDir: { 980 NoParentOwners: true, 981 }, 982 noParentsFilterDir: { 983 NoParentOwners: true, 984 }, 985 }, 986 } 987 tests := []struct { 988 name string 989 filePath string 990 expectedOwnersPath string 991 expectedLeafOwners sets.Set[string] 992 expectedAllOwners sets.Set[string] 993 }{ 994 { 995 name: "Modified Base Dir Only", 996 filePath: filepath.Join(baseDir, "testFile.md"), 997 expectedOwnersPath: baseDir, 998 expectedLeafOwners: ro.approvers[baseDir][nil], 999 expectedAllOwners: ro.approvers[baseDir][nil], 1000 }, 1001 { 1002 name: "Modified Leaf Dir Only", 1003 filePath: filepath.Join(leafDir, "testFile.md"), 1004 expectedOwnersPath: leafDir, 1005 expectedLeafOwners: ro.approvers[leafDir][nil], 1006 expectedAllOwners: ro.approvers[baseDir][nil].Union(ro.approvers[leafDir][nil]), 1007 }, 1008 { 1009 name: "Modified regexp matched file in Leaf Dir Only", 1010 filePath: filepath.Join(leafFilterDir, "testFile.md"), 1011 expectedOwnersPath: leafFilterDir, 1012 expectedLeafOwners: ro.approvers[leafFilterDir][mdFileReg], 1013 expectedAllOwners: ro.approvers[baseDir][nil].Union(ro.approvers[leafFilterDir][mdFileReg]), 1014 }, 1015 { 1016 name: "Modified not regexp matched file in Leaf Dir Only", 1017 filePath: filepath.Join(leafFilterDir, "testFile.dat"), 1018 expectedOwnersPath: baseDir, 1019 expectedLeafOwners: ro.approvers[baseDir][nil], 1020 expectedAllOwners: ro.approvers[baseDir][nil], 1021 }, 1022 { 1023 name: "Modified NoParentOwners Dir Only", 1024 filePath: filepath.Join(noParentsDir, "testFile.go"), 1025 expectedOwnersPath: noParentsDir, 1026 expectedLeafOwners: ro.approvers[noParentsDir][nil], 1027 expectedAllOwners: ro.approvers[noParentsDir][nil], 1028 }, 1029 { 1030 name: "Modified regexp matched file NoParentOwners Dir Only", 1031 filePath: filepath.Join(noParentsFilterDir, "testFile.txt"), 1032 expectedOwnersPath: noParentsFilterDir, 1033 expectedLeafOwners: ro.approvers[noParentsFilterDir][txtFileReg], 1034 expectedAllOwners: ro.approvers[noParentsFilterDir][txtFileReg], 1035 }, 1036 { 1037 name: "Modified regexp not matched file in NoParentOwners Dir Only", 1038 filePath: filepath.Join(noParentsFilterDir, "testFile.go_to_parent"), 1039 expectedOwnersPath: baseDir, 1040 expectedLeafOwners: ro.approvers[baseDir][nil], 1041 expectedAllOwners: ro.approvers[baseDir][nil], 1042 }, 1043 { 1044 name: "Modified Nonexistent Dir (Default to Base)", 1045 filePath: filepath.Join(nonExistentDir, "testFile.md"), 1046 expectedOwnersPath: baseDir, 1047 expectedLeafOwners: ro.approvers[baseDir][nil], 1048 expectedAllOwners: ro.approvers[baseDir][nil], 1049 }, 1050 } 1051 for testNum, test := range tests { 1052 foundLeafApprovers := ro.LeafApprovers(test.filePath) 1053 foundApprovers := ro.Approvers(test.filePath).Set() 1054 foundOwnersPath := ro.FindApproverOwnersForFile(test.filePath) 1055 if !foundLeafApprovers.Equal(test.expectedLeafOwners) { 1056 t.Errorf("The Leaf Approvers Found Do Not Match Expected For Test %d: %s", testNum, test.name) 1057 t.Errorf("\tExpected Owners: %v\tFound Owners: %v ", test.expectedLeafOwners, foundLeafApprovers) 1058 } 1059 if !foundApprovers.Equal(test.expectedAllOwners) { 1060 t.Errorf("The Approvers Found Do Not Match Expected For Test %d: %s", testNum, test.name) 1061 t.Errorf("\tExpected Owners: %v\tFound Owners: %v ", test.expectedAllOwners, foundApprovers) 1062 } 1063 if foundOwnersPath != test.expectedOwnersPath { 1064 t.Errorf("The Owners Path Found Does Not Match Expected For Test %d: %s", testNum, test.name) 1065 t.Errorf("\tExpected Owners: %v\tFound Owners: %v ", test.expectedOwnersPath, foundOwnersPath) 1066 } 1067 } 1068 } 1069 1070 func TestFindLabelsForPath(t *testing.T) { 1071 tests := []struct { 1072 name string 1073 path string 1074 expectedLabels sets.Set[string] 1075 }{ 1076 { 1077 name: "base 1", 1078 path: "foo.txt", 1079 expectedLabels: sets.New[string]("sig/godzilla"), 1080 }, { 1081 name: "base 2", 1082 path: "./foo.txt", 1083 expectedLabels: sets.New[string]("sig/godzilla"), 1084 }, { 1085 name: "base 3", 1086 path: "", 1087 expectedLabels: sets.New[string]("sig/godzilla"), 1088 }, { 1089 name: "base 4", 1090 path: ".", 1091 expectedLabels: sets.New[string]("sig/godzilla"), 1092 }, { 1093 name: "leaf 1", 1094 path: "a/b/c/foo.txt", 1095 expectedLabels: sets.New[string]("sig/godzilla", "wg/save-tokyo"), 1096 }, { 1097 name: "leaf 2", 1098 path: "a/b/foo.txt", 1099 expectedLabels: sets.New[string]("sig/godzilla"), 1100 }, 1101 } 1102 1103 testOwners := &RepoOwners{ 1104 labels: map[string]map[*regexp.Regexp]sets.Set[string]{ 1105 baseDir: regexpAll("sig/godzilla"), 1106 leafDir: regexpAll("wg/save-tokyo"), 1107 }, 1108 } 1109 for _, test := range tests { 1110 got := testOwners.FindLabelsForFile(test.path) 1111 if !got.Equal(test.expectedLabels) { 1112 t.Errorf( 1113 "[%s] Expected labels %q for path %q, but got %q.", 1114 test.name, 1115 sets.List(test.expectedLabels), 1116 test.path, 1117 sets.List(got), 1118 ) 1119 } 1120 } 1121 } 1122 1123 func TestCanonicalize(t *testing.T) { 1124 tests := []struct { 1125 name string 1126 path string 1127 expectedPath string 1128 }{ 1129 { 1130 name: "Empty String", 1131 path: "", 1132 expectedPath: "", 1133 }, 1134 { 1135 name: "Dot (.) as Path", 1136 path: ".", 1137 expectedPath: "", 1138 }, 1139 { 1140 name: "GitHub Style Input (No Root)", 1141 path: "a/b/c/d.txt", 1142 expectedPath: "a/b/c/d.txt", 1143 }, 1144 { 1145 name: "Preceding Slash and Trailing Slash", 1146 path: "/a/b/", 1147 expectedPath: "/a/b", 1148 }, 1149 { 1150 name: "Trailing Slash", 1151 path: "foo/bar/baz/", 1152 expectedPath: "foo/bar/baz", 1153 }, 1154 } 1155 for _, test := range tests { 1156 if got := canonicalize(test.path); test.expectedPath != got { 1157 t.Errorf( 1158 "[%s] Expected the canonical path for %v to be %v. Found %v instead", 1159 test.name, 1160 test.path, 1161 test.expectedPath, 1162 got, 1163 ) 1164 } 1165 } 1166 } 1167 1168 func TestExpandAliases(t *testing.T) { 1169 testAliases := RepoAliases{ 1170 "team/t1": sets.New[string]("u1", "u2"), 1171 "team/t2": sets.New[string]("u1", "u3"), 1172 "team/t3": sets.New[string](), 1173 } 1174 tests := []struct { 1175 name string 1176 unexpanded sets.Set[string] 1177 expectedExpanded sets.Set[string] 1178 }{ 1179 { 1180 name: "No expansions.", 1181 unexpanded: sets.New[string]("abc", "def"), 1182 expectedExpanded: sets.New[string]("abc", "def"), 1183 }, 1184 { 1185 name: "One alias to be expanded", 1186 unexpanded: sets.New[string]("abc", "team/t1"), 1187 expectedExpanded: sets.New[string]("abc", "u1", "u2"), 1188 }, 1189 { 1190 name: "Duplicates inside and outside alias.", 1191 unexpanded: sets.New[string]("u1", "team/t1"), 1192 expectedExpanded: sets.New[string]("u1", "u2"), 1193 }, 1194 { 1195 name: "Duplicates in multiple aliases.", 1196 unexpanded: sets.New[string]("u1", "team/t1", "team/t2"), 1197 expectedExpanded: sets.New[string]("u1", "u2", "u3"), 1198 }, 1199 { 1200 name: "Mixed casing in aliases.", 1201 unexpanded: sets.New[string]("Team/T1"), 1202 expectedExpanded: sets.New[string]("u1", "u2"), 1203 }, 1204 { 1205 name: "Empty team.", 1206 unexpanded: sets.New[string]("Team/T3"), 1207 expectedExpanded: sets.New[string](), 1208 }, 1209 } 1210 1211 for _, test := range tests { 1212 if got := testAliases.ExpandAliases(test.unexpanded); !test.expectedExpanded.Equal(got) { 1213 t.Errorf( 1214 "[%s] Expected %q to expand to %q, but got %q.", 1215 test.name, 1216 sets.List(test.unexpanded), 1217 sets.List(test.expectedExpanded), 1218 sets.List(got), 1219 ) 1220 } 1221 } 1222 } 1223 1224 func TestSaveSimpleConfig(t *testing.T) { 1225 dir := t.TempDir() 1226 1227 tests := []struct { 1228 name string 1229 given SimpleConfig 1230 expected string 1231 }{ 1232 { 1233 name: "No expansions.", 1234 given: SimpleConfig{ 1235 Config: Config{ 1236 Approvers: []string{"david", "sig-alias", "Alice"}, 1237 Reviewers: []string{"adam", "sig-alias"}, 1238 }, 1239 }, 1240 expected: `approvers: 1241 - david 1242 - sig-alias 1243 - Alice 1244 options: {} 1245 reviewers: 1246 - adam 1247 - sig-alias 1248 `, 1249 }, 1250 } 1251 1252 for _, test := range tests { 1253 file := filepath.Join(dir, fmt.Sprintf("%s.yaml", test.name)) 1254 err := SaveSimpleConfig(test.given, file) 1255 if err != nil { 1256 t.Errorf("unexpected error when writing simple config") 1257 } 1258 b, err := os.ReadFile(file) 1259 if err != nil { 1260 t.Errorf("unexpected error when reading file: %s", file) 1261 } 1262 s := string(b) 1263 if test.expected != s { 1264 t.Errorf("result '%s' is differ from expected: '%s'", s, test.expected) 1265 } 1266 simple, err := LoadSimpleConfig(b) 1267 if err != nil { 1268 t.Errorf("unexpected error when load simple config: %v", err) 1269 } 1270 if !reflect.DeepEqual(simple, test.given) { 1271 t.Errorf("unexpected error when loading simple config from: '%s'", diff.ObjectReflectDiff(simple, test.given)) 1272 } 1273 } 1274 } 1275 1276 func TestSaveFullConfig(t *testing.T) { 1277 dir := t.TempDir() 1278 1279 tests := []struct { 1280 name string 1281 given FullConfig 1282 expected string 1283 }{ 1284 { 1285 name: "No expansions.", 1286 given: FullConfig{ 1287 Filters: map[string]Config{ 1288 ".*": { 1289 Approvers: []string{"alice", "bob", "carol", "david"}, 1290 Reviewers: []string{"adam", "bob", "carol"}, 1291 }, 1292 }, 1293 }, 1294 expected: `filters: 1295 .*: 1296 approvers: 1297 - alice 1298 - bob 1299 - carol 1300 - david 1301 reviewers: 1302 - adam 1303 - bob 1304 - carol 1305 options: {} 1306 `, 1307 }, 1308 } 1309 1310 for _, test := range tests { 1311 file := filepath.Join(dir, fmt.Sprintf("%s.yaml", test.name)) 1312 err := SaveFullConfig(test.given, file) 1313 if err != nil { 1314 t.Errorf("unexpected error when writing full config") 1315 } 1316 b, err := os.ReadFile(file) 1317 if err != nil { 1318 t.Errorf("unexpected error when reading file: %s", file) 1319 } 1320 s := string(b) 1321 if test.expected != s { 1322 t.Errorf("result '%s' is differ from expected: '%s'", s, test.expected) 1323 } 1324 full, err := LoadFullConfig(b) 1325 if err != nil { 1326 t.Errorf("unexpected error when load full config: %v", err) 1327 } 1328 if !reflect.DeepEqual(full, test.given) { 1329 t.Errorf("unexpected error when loading simple config from: '%s'", diff.ObjectReflectDiff(full, test.given)) 1330 } 1331 } 1332 } 1333 1334 func TestTopLevelApprovers(t *testing.T) { 1335 expectedApprovers := []string{"alice", "bob"} 1336 ro := &RepoOwners{ 1337 approvers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1338 baseDir: regexpAll(expectedApprovers...), 1339 leafDir: regexpAll("carl", "dave"), 1340 }, 1341 } 1342 1343 foundApprovers := ro.TopLevelApprovers() 1344 if !foundApprovers.Equal(sets.New[string](expectedApprovers...)) { 1345 t.Errorf("Expected Owners: %v\tFound Owners: %v ", expectedApprovers, foundApprovers) 1346 } 1347 } 1348 1349 func TestCacheDoesntRace(t *testing.T) { 1350 key := "key" 1351 cache := newCache() 1352 1353 wg := &sync.WaitGroup{} 1354 wg.Add(2) 1355 1356 go func() { cache.setEntry(key, cacheEntry{}); wg.Done() }() 1357 go func() { cache.getEntry(key); wg.Done() }() 1358 1359 wg.Wait() 1360 } 1361 1362 func TestRepoOwners_AllOwners(t *testing.T) { 1363 expectedOwners := []string{"alice", "bob", "cjwagner", "matthyx", "mml"} 1364 ro := &RepoOwners{ 1365 approvers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1366 "": regexpAll("cjwagner"), 1367 "src": regexpAll(), 1368 "src/dir": regexpAll("bob"), 1369 "src/dir/conformance": regexpAll("mml"), 1370 "src/dir/subdir": regexpAll("alice", "bob"), 1371 "vendor": regexpAll("alice"), 1372 }, 1373 reviewers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1374 "": regexpAll("alice", "bob"), 1375 "src/dir": regexpAll("alice", "matthyx"), 1376 "src/dir/subdir": regexpAll("alice", "bob"), 1377 }, 1378 } 1379 foundOwners := ro.AllOwners() 1380 if !foundOwners.Equal(sets.New[string](expectedOwners...)) { 1381 t.Errorf("Expected Owners: %v\tFound Owners: %v ", expectedOwners, sets.List(foundOwners)) 1382 } 1383 } 1384 1385 func TestRepoOwners_AllApprovers(t *testing.T) { 1386 expectedApprovers := []string{"alice", "bob", "cjwagner", "mml"} 1387 ro := &RepoOwners{ 1388 approvers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1389 "": regexpAll("cjwagner"), 1390 "src": regexpAll(), 1391 "src/dir": regexpAll("bob"), 1392 "src/dir/conformance": regexpAll("mml"), 1393 "src/dir/subdir": regexpAll("alice", "bob"), 1394 "vendor": regexpAll("alice"), 1395 }, 1396 reviewers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1397 "": regexpAll("alice", "bob"), 1398 "src/dir": regexpAll("alice", "matthyx"), 1399 "src/dir/subdir": regexpAll("alice", "bob"), 1400 }, 1401 } 1402 foundApprovers := ro.AllApprovers() 1403 if !foundApprovers.Equal(sets.New[string](expectedApprovers...)) { 1404 t.Errorf("Expected approvers: %v\tFound approvers: %v ", expectedApprovers, sets.List(foundApprovers)) 1405 } 1406 } 1407 1408 func TestRepoOwners_AllReviewers(t *testing.T) { 1409 expectedReviewers := []string{"alice", "bob", "matthyx"} 1410 ro := &RepoOwners{ 1411 approvers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1412 "": regexpAll("cjwagner"), 1413 "src": regexpAll(), 1414 "src/dir": regexpAll("bob"), 1415 "src/dir/conformance": regexpAll("mml"), 1416 "src/dir/subdir": regexpAll("alice", "bob"), 1417 "vendor": regexpAll("alice"), 1418 }, 1419 reviewers: map[string]map[*regexp.Regexp]sets.Set[string]{ 1420 "": regexpAll("alice", "bob"), 1421 "src/dir": regexpAll("alice", "matthyx"), 1422 "src/dir/subdir": regexpAll("alice", "bob"), 1423 }, 1424 } 1425 foundReviewers := ro.AllReviewers() 1426 if !foundReviewers.Equal(sets.New[string](expectedReviewers...)) { 1427 t.Errorf("Expected reviewers: %v\tFound reviewers: %v ", expectedReviewers, sets.List(foundReviewers)) 1428 } 1429 }