From ad3cd1f3c78082fa3b89af40ac12fe9fcd022f85 Mon Sep 17 00:00:00 2001 From: Jordan Coin Jackson Date: Tue, 3 Mar 2026 09:54:33 -0500 Subject: [PATCH 1/2] test: improve scanner coverage from 59.2% to 73.1% --- .github/workflows/ci.yml | 2 +- scanner/deps_test.go | 268 +++++++++++++++++++++++++++++++++++++++ scanner/walker_test.go | 148 +++++++++++++++++++++ 3 files changed, 417 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca2270e..843f670 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,7 @@ jobs: run: | total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') # Floor target: 90%. Codex PRs will incrementally raise this value. - min=30.0 + min=35.0 awk -v t="$total" -v m="$min" 'BEGIN { if (t+0 < m+0) { printf "Coverage %.1f%% is below floor %.1f%%\n", t, m diff --git a/scanner/deps_test.go b/scanner/deps_test.go index a4af78e..5fb80c2 100644 --- a/scanner/deps_test.go +++ b/scanner/deps_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "reflect" "sort" + "strings" "testing" ) @@ -493,3 +494,270 @@ func TestResolvePathAliasNoMatch(t *testing.T) { t.Errorf("Expected no results for non-existent file, got %v", result) } } + +func TestBuildFileIndex(t *testing.T) { + files := []FileInfo{ + {Path: "main.go"}, + {Path: "pkg/service/handler.go"}, + {Path: "src/modules/auth/index.ts"}, + } + + idx := buildFileIndex(files, "example.com/project") + + if got := idx.byDir["pkg/service"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + t.Fatalf("expected pkg/service/handler.go in byDir, got %v", got) + } + if got := idx.byExact["pkg/service/handler"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + t.Fatalf("expected no-ext exact match for handler.go, got %v", got) + } + if got := idx.bySuffix["service/handler.go"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + t.Fatalf("expected suffix match for service/handler.go, got %v", got) + } + if got := idx.goPkgs["example.com/project/pkg/service"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + t.Fatalf("expected go package index for pkg/service, got %v", got) + } +} + +func TestNormalizeImport(t *testing.T) { + tests := []struct { + name string + imp string + want string + }{ + {name: "trims quotes", imp: "\"pkg/util\"", want: "pkg/util"}, + {name: "python dotted path", imp: "app.core.config", want: filepath.Join("app", "core", "config")}, + {name: "crate path", imp: "crate::net::http", want: filepath.Join("net", "http")}, + {name: "super path", imp: "super::service::api", want: filepath.Join("super", "service", "api")}, + {name: "already slash path", imp: "src/util", want: "src/util"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := normalizeImport(tt.imp) + if got != tt.want { + t.Fatalf("normalizeImport(%q): want %q, got %q", tt.imp, tt.want, got) + } + }) + } +} + +func TestResolveRelative(t *testing.T) { + files := []FileInfo{ + {Path: "pkg/api/handler.go"}, + {Path: "pkg/common/types.go"}, + {Path: "pkg/log/logger.go"}, + } + idx := buildFileIndex(files, "") + + tests := []struct { + name string + imp string + fromDir string + want []string + }{ + {name: "same directory file", imp: "./handler", fromDir: "pkg/api", want: []string{"pkg/api/handler.go"}}, + {name: "parent directory file", imp: "../common/types", fromDir: "pkg/api", want: []string{"pkg/common/types.go"}}, + {name: "two levels up", imp: "../../log/logger", fromDir: "pkg/api/internal", want: []string{"pkg/log/logger.go"}}, + {name: "missing file", imp: "./missing", fromDir: "pkg/api", want: nil}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := resolveRelative(tt.imp, tt.fromDir, idx) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("resolveRelative(%q, %q): want %v, got %v", tt.imp, tt.fromDir, tt.want, got) + } + }) + } +} + +func TestFuzzyResolve(t *testing.T) { + files := []FileInfo{ + {Path: "pkg/service/handler.go"}, + {Path: "src/modules/auth/login.ts"}, + {Path: "src/shared/utils/helpers.ts"}, + {Path: "app/core/config.py"}, + } + idx := buildFileIndex(files, "example.com/project") + aliases := map[string][]string{ + "@modules/*": {"src/modules/*"}, + } + + tests := []struct { + name string + imp string + fromFile string + goModule string + pathAlias map[string][]string + baseURL string + want []string + }{ + { + name: "go package lookup", + imp: "example.com/project/pkg/service", + fromFile: "cmd/main.go", + goModule: "example.com/project", + pathAlias: nil, + baseURL: "", + want: []string{"pkg/service/handler.go"}, + }, + { + name: "relative import", + imp: "../service/handler", + fromFile: "pkg/api/router.go", + goModule: "example.com/project", + pathAlias: nil, + baseURL: "", + want: []string{"pkg/service/handler.go"}, + }, + { + name: "alias import", + imp: "@modules/auth/login", + fromFile: "src/app.ts", + goModule: "example.com/project", + pathAlias: aliases, + baseURL: ".", + want: []string{"src/modules/auth/login.ts"}, + }, + { + name: "exact import", + imp: "src/shared/utils/helpers", + fromFile: "src/app.ts", + goModule: "example.com/project", + pathAlias: nil, + baseURL: "", + want: []string{"src/shared/utils/helpers.ts"}, + }, + { + name: "suffix import", + imp: "core.config", + fromFile: "app/main.py", + goModule: "example.com/project", + pathAlias: nil, + baseURL: "", + want: []string{"app/core/config.py"}, + }, + { + name: "no match", + imp: "github.com/external/lib", + fromFile: "main.go", + goModule: "example.com/project", + pathAlias: nil, + baseURL: "", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := fuzzyResolve(tt.imp, tt.fromFile, idx, tt.goModule, tt.pathAlias, tt.baseURL) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("fuzzyResolve(%q): want %v, got %v", tt.imp, tt.want, got) + } + }) + } +} + +func TestDetectModule(t *testing.T) { + tests := []struct { + name string + goModBody string + writeGoMod bool + want string + }{ + { + name: "module found", + goModBody: strings.Join([]string{ + "module example.com/project", + "", + "go 1.22", + }, "\n"), + writeGoMod: true, + want: "example.com/project", + }, + { + name: "missing go.mod", + writeGoMod: false, + want: "", + }, + { + name: "go.mod without module", + goModBody: strings.Join([]string{ + "go 1.22", + }, "\n"), + writeGoMod: true, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + if tt.writeGoMod { + err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(tt.goModBody), 0o644) + if err != nil { + t.Fatalf("write go.mod: %v", err) + } + } + + got := detectModule(dir) + if got != tt.want { + t.Fatalf("detectModule(): want %q, got %q", tt.want, got) + } + }) + } +} + +func TestFileGraphHubAndConnectedFiles(t *testing.T) { + fg := &FileGraph{ + Imports: map[string][]string{ + "a.go": {"hub.go", "c.go"}, + "b.go": {"hub.go"}, + }, + Importers: map[string][]string{ + "hub.go": {"a.go", "b.go", "d.go"}, + "a.go": {"x.go"}, + }, + } + + if !fg.IsHub("hub.go") { + t.Fatal("expected hub.go to be treated as hub") + } + if fg.IsHub("a.go") { + t.Fatal("did not expect a.go to be treated as hub") + } + + hubs := fg.HubFiles() + if len(hubs) != 1 || hubs[0] != "hub.go" { + t.Fatalf("expected only hub.go as hub, got %v", hubs) + } + + connected := fg.ConnectedFiles("a.go") + sort.Strings(connected) + want := []string{"c.go", "hub.go", "x.go"} + if !reflect.DeepEqual(connected, want) { + t.Fatalf("expected connected files %v, got %v", want, connected) + } +} + +func TestDetectLanguage(t *testing.T) { + tests := []struct { + name string + path string + want string + }{ + {name: "go file", path: "main.go", want: "go"}, + {name: "typescript upper extension", path: "comp.TSX", want: "typescript"}, + {name: "scala", path: "build.sc", want: "scala"}, + {name: "unknown extension", path: "README.md", want: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DetectLanguage(tt.path) + if got != tt.want { + t.Fatalf("DetectLanguage(%q): want %q, got %q", tt.path, tt.want, got) + } + }) + } +} diff --git a/scanner/walker_test.go b/scanner/walker_test.go index b9bc454..1b952ae 100644 --- a/scanner/walker_test.go +++ b/scanner/walker_test.go @@ -3,6 +3,8 @@ package scanner import ( "os" "path/filepath" + "reflect" + "sort" "testing" ) @@ -564,3 +566,149 @@ func TestNestedGitignoreDirectoryIgnore(t *testing.T) { t.Errorf("Expected 3 files, got %d: %v", len(files), foundPaths) } } + +func TestMatchesPattern(t *testing.T) { + tests := []struct { + name string + relPath string + pattern string + want bool + }{ + {name: "glob filename match", relPath: "src/user_test.go", pattern: "*_test.go", want: true}, + {name: "glob path match", relPath: "assets/icons/logo.svg", pattern: "assets/*/*.svg", want: true}, + {name: "extension with dot", relPath: "assets/logo.png", pattern: ".png", want: true}, + {name: "extension without dot", relPath: "assets/logo.PNG", pattern: "png", want: true}, + {name: "directory component", relPath: "src/Fonts/Inter.ttf", pattern: "Fonts", want: true}, + {name: "exact directory path", relPath: "Fonts", pattern: "Fonts", want: true}, + {name: "no match", relPath: "src/main.go", pattern: "images", want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := matchesPattern(tt.relPath, tt.pattern) + if got != tt.want { + t.Fatalf("matchesPattern(%q, %q): want %v, got %v", tt.relPath, tt.pattern, tt.want, got) + } + }) + } +} + +func TestShouldIncludeFile(t *testing.T) { + tests := []struct { + name string + relPath string + ext string + only []string + exclude []string + want bool + }{ + { + name: "no filters includes file", + relPath: "src/main.go", + ext: ".go", + only: nil, + exclude: nil, + want: true, + }, + { + name: "only filter match", + relPath: "src/main.go", + ext: ".go", + only: []string{"go", "ts"}, + exclude: nil, + want: true, + }, + { + name: "only filter no match", + relPath: "src/main.py", + ext: ".py", + only: []string{"go", "ts"}, + exclude: nil, + want: false, + }, + { + name: "exclude extension pattern", + relPath: "assets/logo.png", + ext: ".png", + only: nil, + exclude: []string{".png"}, + want: false, + }, + { + name: "exclude directory pattern", + relPath: "src/generated/file.go", + ext: ".go", + only: []string{"go"}, + exclude: []string{"generated"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shouldIncludeFile(tt.relPath, tt.ext, tt.only, tt.exclude) + if got != tt.want { + t.Fatalf("shouldIncludeFile(%q, %q): want %v, got %v", tt.relPath, tt.ext, tt.want, got) + } + }) + } +} + +func TestEnsureDir(t *testing.T) { + root := t.TempDir() + nested := filepath.Join(root, "nested") + if err := os.MkdirAll(nested, 0o755); err != nil { + t.Fatal(err) + } + + cache := NewGitIgnoreCache(root) + if err := os.WriteFile(filepath.Join(nested, ".gitignore"), []byte("*.tmp\n"), 0o644); err != nil { + t.Fatal(err) + } + + cache.EnsureDir(nested) + if _, ok := cache.patterns[nested]; !ok { + t.Fatal("expected EnsureDir to load nested .gitignore patterns") + } + if !cache.ShouldIgnore(filepath.Join(nested, "file.tmp")) { + t.Fatal("expected nested .gitignore pattern to apply") + } + + var nilCache *GitIgnoreCache + nilCache.EnsureDir(nested) +} + +func TestScanFilesWithOnlyAndExcludeFilters(t *testing.T) { + tmpDir := t.TempDir() + files := []string{ + "cmd/main.go", + "cmd/main_test.go", + "pkg/data.json", + "docs/readme.md", + } + for _, f := range files { + full := filepath.Join(tmpDir, f) + if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(full, []byte("content"), 0o644); err != nil { + t.Fatal(err) + } + } + + got, err := ScanFiles(tmpDir, nil, []string{"go"}, []string{"*_test.go"}) + if err != nil { + t.Fatal(err) + } + + var paths []string + for _, f := range got { + paths = append(paths, filepath.ToSlash(f.Path)) + } + sort.Strings(paths) + + want := []string{"cmd/main.go"} + if !reflect.DeepEqual(paths, want) { + t.Fatalf("expected %v, got %v", want, paths) + } +} From c56a8010f9f018fe255f2846def66d262abb08c9 Mon Sep 17 00:00:00 2001 From: Jordan Coin Jackson Date: Wed, 4 Mar 2026 11:16:49 -0500 Subject: [PATCH 2/2] test: address PR review feedback on path portability --- .github/workflows/ci.yml | 2 +- scanner/deps_test.go | 67 ++++++++++++++++++++++++---------------- scanner/walker_test.go | 11 +++++-- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 843f670..d605293 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.23' run: | total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') - # Floor target: 90%. Codex PRs will incrementally raise this value. + # Long-term floor target is 90%; this run enforces the current incremental floor. min=35.0 awk -v t="$total" -v m="$min" 'BEGIN { if (t+0 < m+0) { diff --git a/scanner/deps_test.go b/scanner/deps_test.go index 5fb80c2..5cd9357 100644 --- a/scanner/deps_test.go +++ b/scanner/deps_test.go @@ -496,24 +496,28 @@ func TestResolvePathAliasNoMatch(t *testing.T) { } func TestBuildFileIndex(t *testing.T) { + handlerPath := filepath.FromSlash("pkg/service/handler.go") files := []FileInfo{ {Path: "main.go"}, - {Path: "pkg/service/handler.go"}, - {Path: "src/modules/auth/index.ts"}, + {Path: handlerPath}, + {Path: filepath.FromSlash("src/modules/auth/index.ts")}, } idx := buildFileIndex(files, "example.com/project") - if got := idx.byDir["pkg/service"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { - t.Fatalf("expected pkg/service/handler.go in byDir, got %v", got) + handlerDir := filepath.Dir(handlerPath) + if got := idx.byDir[handlerDir]; len(got) != 1 || got[0] != handlerPath { + t.Fatalf("expected %q in byDir, got %v", handlerPath, got) } - if got := idx.byExact["pkg/service/handler"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + handlerNoExt := strings.TrimSuffix(handlerPath, filepath.Ext(handlerPath)) + if got := idx.byExact[handlerNoExt]; len(got) != 1 || got[0] != handlerPath { t.Fatalf("expected no-ext exact match for handler.go, got %v", got) } - if got := idx.bySuffix["service/handler.go"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + handlerSuffix := filepath.Join("service", "handler.go") + if got := idx.bySuffix[handlerSuffix]; len(got) != 1 || got[0] != handlerPath { t.Fatalf("expected suffix match for service/handler.go, got %v", got) } - if got := idx.goPkgs["example.com/project/pkg/service"]; len(got) != 1 || got[0] != "pkg/service/handler.go" { + if got := idx.goPkgs["example.com/project/pkg/service"]; len(got) != 1 || got[0] != handlerPath { t.Fatalf("expected go package index for pkg/service, got %v", got) } } @@ -542,10 +546,14 @@ func TestNormalizeImport(t *testing.T) { } func TestResolveRelative(t *testing.T) { + handlerPath := filepath.FromSlash("pkg/api/handler.go") + typesPath := filepath.FromSlash("pkg/common/types.go") + loggerPath := filepath.FromSlash("pkg/log/logger.go") + files := []FileInfo{ - {Path: "pkg/api/handler.go"}, - {Path: "pkg/common/types.go"}, - {Path: "pkg/log/logger.go"}, + {Path: handlerPath}, + {Path: typesPath}, + {Path: loggerPath}, } idx := buildFileIndex(files, "") @@ -555,9 +563,9 @@ func TestResolveRelative(t *testing.T) { fromDir string want []string }{ - {name: "same directory file", imp: "./handler", fromDir: "pkg/api", want: []string{"pkg/api/handler.go"}}, - {name: "parent directory file", imp: "../common/types", fromDir: "pkg/api", want: []string{"pkg/common/types.go"}}, - {name: "two levels up", imp: "../../log/logger", fromDir: "pkg/api/internal", want: []string{"pkg/log/logger.go"}}, + {name: "same directory file", imp: "./handler", fromDir: filepath.FromSlash("pkg/api"), want: []string{handlerPath}}, + {name: "parent directory file", imp: "../common/types", fromDir: filepath.FromSlash("pkg/api"), want: []string{typesPath}}, + {name: "two levels up", imp: "../../log/logger", fromDir: filepath.FromSlash("pkg/api/internal"), want: []string{loggerPath}}, {name: "missing file", imp: "./missing", fromDir: "pkg/api", want: nil}, } @@ -572,15 +580,20 @@ func TestResolveRelative(t *testing.T) { } func TestFuzzyResolve(t *testing.T) { + goHandler := filepath.FromSlash("pkg/service/handler.go") + tsLogin := filepath.FromSlash("src/modules/auth/login.ts") + tsHelper := filepath.FromSlash("src/shared/utils/helpers.ts") + pyConfig := filepath.FromSlash("app/core/config.py") + files := []FileInfo{ - {Path: "pkg/service/handler.go"}, - {Path: "src/modules/auth/login.ts"}, - {Path: "src/shared/utils/helpers.ts"}, - {Path: "app/core/config.py"}, + {Path: goHandler}, + {Path: tsLogin}, + {Path: tsHelper}, + {Path: pyConfig}, } idx := buildFileIndex(files, "example.com/project") aliases := map[string][]string{ - "@modules/*": {"src/modules/*"}, + "@modules/*": {filepath.FromSlash("src/modules/*")}, } tests := []struct { @@ -599,43 +612,43 @@ func TestFuzzyResolve(t *testing.T) { goModule: "example.com/project", pathAlias: nil, baseURL: "", - want: []string{"pkg/service/handler.go"}, + want: []string{goHandler}, }, { name: "relative import", imp: "../service/handler", - fromFile: "pkg/api/router.go", + fromFile: filepath.FromSlash("pkg/api/router.go"), goModule: "example.com/project", pathAlias: nil, baseURL: "", - want: []string{"pkg/service/handler.go"}, + want: []string{goHandler}, }, { name: "alias import", imp: "@modules/auth/login", - fromFile: "src/app.ts", + fromFile: filepath.FromSlash("src/app.ts"), goModule: "example.com/project", pathAlias: aliases, baseURL: ".", - want: []string{"src/modules/auth/login.ts"}, + want: []string{tsLogin}, }, { name: "exact import", imp: "src/shared/utils/helpers", - fromFile: "src/app.ts", + fromFile: filepath.FromSlash("src/app.ts"), goModule: "example.com/project", pathAlias: nil, baseURL: "", - want: []string{"src/shared/utils/helpers.ts"}, + want: []string{tsHelper}, }, { name: "suffix import", imp: "core.config", - fromFile: "app/main.py", + fromFile: filepath.FromSlash("app/main.py"), goModule: "example.com/project", pathAlias: nil, baseURL: "", - want: []string{"app/core/config.py"}, + want: []string{pyConfig}, }, { name: "no match", diff --git a/scanner/walker_test.go b/scanner/walker_test.go index 1b952ae..663a805 100644 --- a/scanner/walker_test.go +++ b/scanner/walker_test.go @@ -5,6 +5,7 @@ import ( "path/filepath" "reflect" "sort" + "strings" "testing" ) @@ -585,9 +586,15 @@ func TestMatchesPattern(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := matchesPattern(tt.relPath, tt.pattern) + relPath := filepath.ToSlash(filepath.FromSlash(tt.relPath)) + pattern := tt.pattern + if strings.Contains(pattern, "/") { + pattern = filepath.ToSlash(filepath.FromSlash(pattern)) + } + + got := matchesPattern(relPath, pattern) if got != tt.want { - t.Fatalf("matchesPattern(%q, %q): want %v, got %v", tt.relPath, tt.pattern, tt.want, got) + t.Fatalf("matchesPattern(%q, %q): want %v, got %v", relPath, pattern, tt.want, got) } }) }