package scanner import ( "context" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" ) // buildTree creates a fixture directory tree with a mix of repository types // and noise directories, and returns its root. // // root/ // ├── bare.git/ (bare repository) // ├── deep/nested/repoC/ (working tree, depth 3) // ├── nested/repoB/ (working tree) // ├── node_modules/vendored/ (working tree, exclusion target) // ├── plaindir/ (noise) // ├── repoA/ (working tree) // └── worktrees/wt1/ (linked worktree via .git file) func buildTree(t *testing.T) string { t.Helper() root := t.TempDir() mkdir := func(p string) string { t.Helper() full := filepath.Join(root, p) if err := os.MkdirAll(full, 0o755); err != nil { t.Fatal(err) } return full } runGit := func(dir string, args ...string) { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) } } for _, p := range []string{"repoA", "nested/repoB", "deep/nested/repoC", "node_modules/vendored"} { runGit(mkdir(p), "init", "-q", "-b", "main") } runGit(mkdir("bare.git"), "init", "-q", "--bare") wt := mkdir("worktrees/wt1") if err := os.WriteFile(filepath.Join(wt, ".git"), []byte("gitdir: /nonexistent/real\n"), 0o644); err != nil { t.Fatal(err) } mkdir("plaindir") return root } func TestDiscoverFindsAllRepos(t *testing.T) { root := buildTree(t) d := NewDiscoverer() repos, err := d.Discover(context.Background(), root) if err != nil { t.Fatalf("Discover: %v", err) } want := []string{ filepath.Join(root, "bare.git"), filepath.Join(root, "deep/nested/repoC"), filepath.Join(root, "nested/repoB"), filepath.Join(root, "node_modules/vendored"), filepath.Join(root, "repoA"), filepath.Join(root, "worktrees/wt1"), } if !reflect.DeepEqual(repos, want) { t.Errorf("Discover() =\n %v\nwant\n %v", repos, want) } } func TestDiscoverExcludesPatterns(t *testing.T) { root := buildTree(t) d := NewDiscoverer(WithExclude("node_modules")) repos, err := d.Discover(context.Background(), root) if err != nil { t.Fatalf("Discover: %v", err) } for _, r := range repos { if strings.Contains(r, "node_modules") { t.Errorf("Discover() returned excluded repo %s", r) } } if len(repos) != 5 { t.Errorf("Discover() returned %d repos, want 5", len(repos)) } } func TestDiscoverMaxDepth(t *testing.T) { root := buildTree(t) d := NewDiscoverer(WithMaxDepth(2)) repos, err := d.Discover(context.Background(), root) if err != nil { t.Fatalf("Discover: %v", err) } for _, r := range repos { if strings.Contains(r, "deep/nested/repoC") { t.Errorf("Discover() returned repo beyond max depth: %s", r) } } if len(repos) != 5 { t.Errorf("Discover() returned %d repos, want 5 (deep/nested/repoC excluded)", len(repos)) } } func TestDiscoverBadRoot(t *testing.T) { d := NewDiscoverer() if _, err := d.Discover(context.Background(), filepath.Join(t.TempDir(), "missing")); err == nil { t.Error("Discover(missing) succeeded, want error") } file := filepath.Join(t.TempDir(), "file.txt") if err := os.WriteFile(file, []byte("x"), 0o644); err != nil { t.Fatal(err) } if _, err := d.Discover(context.Background(), file); err == nil { t.Error("Discover(file) succeeded, want error") } } func TestDiscoverCancellation(t *testing.T) { root := buildTree(t) ctx, cancel := context.WithCancel(context.Background()) cancel() // cancelled up front if _, err := NewDiscoverer().Discover(ctx, root); err == nil { t.Error("Discover(cancelled ctx) succeeded, want error") } } func TestDepthBelow(t *testing.T) { root := "/a/b" cases := []struct { path string want int }{ {"/a/b", 0}, {"/a/b/c", 1}, {"/a/b/c/d", 2}, } for _, tc := range cases { if got := depthBelow(root, tc.path); got != tc.want { t.Errorf("depthBelow(%s, %s) = %d, want %d", root, tc.path, got, tc.want) } } }