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  }