package validation import ( "context" "errors" "os" "path/filepath" "strconv" "strings" "testing" ) func TestCheckLocalReplacementsParsesCommittedModuleDeclarations(t *testing.T) { repository := newTreeRepository(t) absoluteTarget := filepath.Join(repository, "outside module") writeTreeFile(t, repository, "go.mod", []byte("module example.com/root\n\nreplace example.com/one => \"../local module\" // retained comment\n")) writeTreeFile(t, repository, "nested/go.mod", []byte("module example.com/nested\n\nreplace (\n\texample.com/two => ../../local\n\texample.com/three => \""+absoluteTarget+"\"\n)\n")) writeTreeFile(t, repository, "vendor/example/go.mod", []byte("module example.com/vendor\n\nreplace example.com/versioned v1.0.0 => example.com/fork v1.2.0\n")) commit := commitCandidateTree(t, repository) tree, err := ReadCandidateTree(context.Background(), repository, commit) if err != nil { t.Fatal(err) } cleanupCandidateTree(t, tree) err = checkLocalReplacements(context.Background(), tree) failure, ok := err.(*Failure) if !ok || failure.Check != LocalReplacements { t.Fatalf("checkLocalReplacements error = %v, want local-replacements failure", err) } want := map[string]Finding{ "go.mod": {Path: "go.mod", Line: 3, Target: "../local module", Directive: "replace"}, "nested/go.mod:4": {Path: "nested/go.mod", Line: 4, Target: "../../local", Directive: "replace"}, "nested/go.mod:5": {Path: "nested/go.mod", Line: 5, Target: absoluteTarget, Directive: "replace"}, } if len(failure.Findings) != len(want) { t.Fatalf("findings = %#v, want %d filesystem replacements", failure.Findings, len(want)) } for _, finding := range failure.Findings { key := finding.Path if finding.Path != "go.mod" { key += ":" + strconv.Itoa(finding.Line) } expected, ok := want[key] if !ok || finding.Line != expected.Line || finding.Target != expected.Target || finding.Directive != expected.Directive { t.Errorf("finding = %#v, want %q", finding, key) } } } func TestCheckLocalReplacementsFollowsCommittedModuleSymlinks(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "modules/real.mod", []byte("module example.com/real\n\nreplace example.com/local => ../checkout\n")) if err := os.MkdirAll(filepath.Join(repository, "nested"), 0o755); err != nil { t.Fatal(err) } if err := os.Symlink("../modules/real.mod", filepath.Join(repository, "nested/go.mod")); err != nil { t.Fatal(err) } commit := commitCandidateTree(t, repository) tree, err := ReadCandidateTree(context.Background(), repository, commit) if err != nil { t.Fatal(err) } cleanupCandidateTree(t, tree) err = checkLocalReplacements(context.Background(), tree) failure, ok := err.(*Failure) if !ok || failure.Check != LocalReplacements || len(failure.Findings) != 1 { t.Fatalf("checkLocalReplacements error = %v, want one symlink declaration finding", err) } finding := failure.Findings[0] if finding.Path != "nested/go.mod" || finding.Line != 3 || finding.Target != "../checkout" { t.Fatalf("symlink finding = %#v, want source path and resolved declaration", finding) } } func TestCheckLocalReplacementsSharesSummariesAcrossModuleSymlinks(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "shared/module.mod", []byte( "module shared.example/module\n"+ "go 1.25.0\n"+ "replace example.org/one => ../one\n"+ "replace example.org/two => ../two\n", )) for _, directory := range []string{"first", "second"} { if err := os.MkdirAll(filepath.Join(repository, directory), 0o755); err != nil { t.Fatal(err) } if err := os.Symlink("../shared/module.mod", filepath.Join(repository, directory, "go.mod")); err != nil { t.Fatal(err) } } commit := commitCandidateTree(t, repository) tree, err := ReadCandidateTree(context.Background(), repository, commit) if err != nil { t.Fatal(err) } cleanupCandidateTree(t, tree) err = checkLocalReplacements(context.Background(), tree) var failure *Failure if !errors.As(err, &failure) || failure.Check != LocalReplacements || len(failure.Findings) != 4 { t.Fatalf("checkLocalReplacements() error = %v, want four reused findings", err) } want := []struct { path string line int target string }{ {"first/go.mod", 3, "../one"}, {"first/go.mod", 4, "../two"}, {"second/go.mod", 3, "../one"}, {"second/go.mod", 4, "../two"}, } for index, finding := range failure.Findings { if finding.Path != want[index].path || finding.Line != want[index].line || finding.Target != want[index].target { t.Errorf("finding[%d] = %#v, want %#v", index, finding, want[index]) } } first, _ := tree.Entry("first/go.mod") second, _ := tree.Entry("second/go.mod") shared, _ := tree.Entry("shared/module.mod") if first.ObjectID != second.ObjectID || first.ObjectID == shared.ObjectID { t.Fatalf("declaration object IDs = %q, %q; module body = %q", first.ObjectID, second.ObjectID, shared.ObjectID) } } func TestCheckLocalReplacementsReportsUnavailableSymlinkContents(t *testing.T) { repository := newTreeRepository(t) if err := os.Symlink("missing.mod", filepath.Join(repository, "go.mod")); err != nil { t.Fatal(err) } commit := commitCandidateTree(t, repository) tree, err := ReadCandidateTree(context.Background(), repository, commit) if err != nil { t.Fatal(err) } cleanupCandidateTree(t, tree) err = checkLocalReplacements(context.Background(), tree) failure, ok := err.(*Failure) if !ok || failure.Check != LocalReplacements || failure.Cause == nil { t.Fatalf("checkLocalReplacements error = %v, want local-replacements inspection failure", err) } if !strings.Contains(failure.Cause.Error(), "missing") || len(failure.Findings) != 1 || failure.Findings[0].Path != "go.mod" { t.Fatalf("failure = %#v, want path and unavailable target cause", failure) } } func TestCheckLocalReplacementsPreservesParseCause(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "go.mod", []byte("module example.com/bad\nreplace =>\n")) commit := commitCandidateTree(t, repository) tree, err := ReadCandidateTree(context.Background(), repository, commit) if err != nil { t.Fatal(err) } cleanupCandidateTree(t, tree) err = checkLocalReplacements(context.Background(), tree) failure, ok := err.(*Failure) if !ok || failure.Check != LocalReplacements || failure.Cause == nil || !strings.Contains(failure.Cause.Error(), "parse committed go.mod") { t.Fatalf("checkLocalReplacements error = %v, want parse failure with cause", err) } if errors.Unwrap(failure) == nil || len(failure.Findings) != 1 || failure.Findings[0].Path != "go.mod" { t.Fatalf("failure = %#v, want preserved parse cause and path", failure) } } func TestRunSelectsDistributionChecksIndependentlyWithGoWorkspaceDisabled(t *testing.T) { t.Setenv("GOWORK", "off") for _, test := range []struct { name string workspaceFiles bool localReplacements bool wantCheck Check }{ {name: "neither"}, {name: "workspace", workspaceFiles: true, wantCheck: WorkspaceFiles}, {name: "replacements", localReplacements: true, wantCheck: LocalReplacements}, {name: "both", workspaceFiles: true, localReplacements: true, wantCheck: WorkspaceFiles}, } { t.Run(test.name, func(t *testing.T) { repository := newTreeRepository(t) writeTreeFile(t, repository, "go.mod", []byte("module example.com/root\nreplace example.com/local => ../local\n")) writeTreeFile(t, repository, "go.work", []byte("go 1.25.0\n")) commit := commitCandidateTree(t, repository) installGoShim(t, "", false) err := Run(context.Background(), Request{ RepositoryDir: repository, Commit: commit, Settings: Settings{ModuleDistribution: ModuleDistributionSettings{ WorkspaceFiles: test.workspaceFiles, LocalReplacements: test.localReplacements, }}, }) if test.wantCheck == "" { if err != nil { t.Fatalf("Run error = %v, want disabled checks", err) } return } failure, ok := err.(*Failure) if !ok || failure.Check != test.wantCheck { t.Fatalf("Run error = %v, want %s failure", err, test.wantCheck) } }) } } func TestRunAttributesCandidateTreeFailureToLocalReplacements(t *testing.T) { repository := newTreeRepository(t) installGoShim(t, "", false) err := Run(context.Background(), Request{ RepositoryDir: repository, Commit: "missing-commit", Settings: Settings{ModuleDistribution: ModuleDistributionSettings{ LocalReplacements: true, }}, }) failure, ok := err.(*Failure) if !ok || failure.Check != LocalReplacements || failure.Cause == nil { t.Fatalf("Run error = %v, want local-replacements candidate inspection failure", err) } } func commitCandidateTree(t testing.TB, repository string) string { t.Helper() gitTree(t, repository, "add", ".") gitTree(t, repository, "commit", "-m", "capture module declarations") return strings.TrimSpace(gitTree(t, repository, "rev-parse", "HEAD")) }