253 lines
9.6 KiB
Go
253 lines
9.6 KiB
Go
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"},
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
|
|
nestedTree, err := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{ModuleDir: filepath.Join(repository, "nested")})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleanupCandidateTree(t, nestedTree)
|
|
err = checkLocalReplacements(context.Background(), nestedTree)
|
|
if !errors.As(err, &failure) || failure.Check != LocalReplacements || len(failure.Findings) != 2 {
|
|
t.Fatalf("nested replacement findings = %#v, want the selected module's two replacements", err)
|
|
}
|
|
for _, finding := range failure.Findings {
|
|
wantLine := 4
|
|
wantTarget := "../../local"
|
|
if finding.Line == 5 {
|
|
wantLine = 5
|
|
wantTarget = absoluteTarget
|
|
}
|
|
if finding.Path != "nested/go.mod" || finding.Line != wantLine || finding.Target != wantTarget || finding.Directive != "replace" {
|
|
t.Errorf("nested finding = %#v, want line %d target %q", finding, wantLine, wantTarget)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{ModuleDir: filepath.Join(repository, "nested")})
|
|
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 := ReadCandidateTreeForScope(context.Background(), repository, commit, TreeScope{
|
|
ModuleDir: repository,
|
|
SharedFiles: []string{"first/go.mod", "second/go.mod"},
|
|
})
|
|
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"))
|
|
}
|