diff --git a/README.md b/README.md index dc0607f..9f00a31 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,15 @@ testrig diagnose --iterations 10 \ ### Native Go You can import `testrig` as a Go package and define your hooks and defaults entirely in Go! See [the pkg.go.dev docs](https://pkg.go.dev/github.com/smartcontractkit/testrig#pkg-examples) for an example setup. + +#### Multi-module repositories + +Import `github.com/smartcontractkit/testrig/modresolve` when you need to run +`go test` or `go list` from a submodule directory: + +```go +moduleDir, patterns, err := modresolve.ResolvePatterns(repoRoot, []string{"./deployment/..."}) +// moduleDir = "/deployment", patterns = []string{"./..."} +``` + +For full `go test` argument slices (flags + patterns), use `ResolveArgs`. diff --git a/go.mod b/go.mod index 7dd1abb..8039b68 100644 --- a/go.mod +++ b/go.mod @@ -16,15 +16,15 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - golang.org/x/sync v0.20.0 - golang.org/x/term v0.43.0 + golang.org/x/sync v0.21.0 + golang.org/x/term v0.44.0 ) require ( github.com/bitfield/gotestdox v0.2.2 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect @@ -51,10 +51,10 @@ require ( github.com/smartcontractkit/testrig/tools/test v0.0.0-00010101000000-000000000000 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.38.0 // indirect + golang.org/x/tools v0.45.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/gotestsum v1.13.0 // indirect diff --git a/go.sum b/go.sum index 53ba49c..717b163 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,12 @@ github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CD github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f h1:vKsPSlO4g4jKfJ9enESgNZ45BkbHngTIq3UxNOzic74= -github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= +github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa h1:rRT2qwk9xbontVloCXEUIsl1ePz0XFcIWkGi2bvmSTY= +github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a h1:aVvnksCVgxB2igk7jERL9ARIkbDXccp1gXCFqhGlamQ= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5 h1:Xl3+pllTbd0iZWeTQixTHClROwU/Gs79ANuOGILkA5g= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= @@ -88,18 +88,18 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/runner/diagnose_progress.go b/internal/runner/diagnose_progress.go index 6e7ac58..52acc4f 100644 --- a/internal/runner/diagnose_progress.go +++ b/internal/runner/diagnose_progress.go @@ -14,60 +14,6 @@ import ( "github.com/smartcontractkit/testrig/internal/termstyle" ) -// testBinaryTwoArgSuffixFlags are test-binary flags that consume the following argv token. -// When scanning backwards from the end, a token immediately after one of these is skipped -// so package patterns can appear before `-run TestName` (valid `go test` ordering). -var testBinaryTwoArgSuffixFlags = map[string]bool{ - "-run": true, - "-bench": true, - "-skip": true, - "-fuzz": true, -} - -func singleArgTestBinaryFlagPrefix(arg string) (prefix string, ok bool) { - for _, p := range []string{"-run=", "-bench=", "-skip=", "-fuzz="} { - if strings.HasPrefix(arg, p) { - return p, true - } - } - return "", false -} - -func looksLikeGoPackagePattern(arg string) bool { - return strings.Contains(arg, ".") || - strings.Contains(arg, "/") || - strings.Contains(arg, "...") -} - -// packagePatternsFromEnd returns trailing arguments that look like package patterns. -// It scans backward from the end of goTestFlagsBeforeArgs(args), skipping `-run`, -// `-bench`, `-skip`, and `-fuzz` and their values so `./pkg -run TestName` still -// yields `./pkg`. This matches the usual `go test [flags] [packages]` layout and -// also package-first ordering with test flags after packages. -func packagePatternsFromEnd(args []string) []string { - args = goTestFlagsBeforeArgs(args) - var pkgs []string - for i := len(args) - 1; i >= 0; i-- { - arg := args[i] - if _, ok := singleArgTestBinaryFlagPrefix(arg); ok { - continue - } - if strings.HasPrefix(arg, "-") { - break - } - if i >= 1 && testBinaryTwoArgSuffixFlags[args[i-1]] { - i-- - continue - } - if !looksLikeGoPackagePattern(arg) { - break - } - pkgs = append(pkgs, arg) - } - slices.Reverse(pkgs) - return pkgs -} - type parallelDiagnoseProgress struct { mu sync.Mutex renderMu sync.Mutex diff --git a/internal/runner/diagnose_results_dir.go b/internal/runner/diagnose_results_dir.go index cd24389..fda2c5f 100644 --- a/internal/runner/diagnose_results_dir.go +++ b/internal/runner/diagnose_results_dir.go @@ -4,6 +4,8 @@ import ( "strings" "time" "unicode/utf8" + + "github.com/smartcontractkit/testrig/modresolve" ) const ( @@ -14,7 +16,7 @@ const ( // parseGoTestRunPattern returns the last `-run` pattern from go test-style argv // (before `-args`), mirroring how Go applies repeated `-run` flags. func parseGoTestRunPattern(goTestArgs []string) string { - args := goTestFlagsBeforeArgs(goTestArgs) + args := modresolve.GoTestFlagsBeforeArgs(goTestArgs) var last string for i := 0; i < len(args); i++ { a := args[i] @@ -88,7 +90,7 @@ func sanitizeDirToken(s string) string { // guessPackagePatternForSlug picks a human-readable slug from go test arguments // (trailing package patterns). Falls back to "pkgs" if none found. func guessPackagePatternForSlug(goTestArgs []string) string { - pkgs := packagePatternsFromEnd(goTestArgs) + pkgs := modresolve.PackagePatternsFromEnd(goTestArgs) switch len(pkgs) { case 0: return "pkgs" diff --git a/internal/runner/module.go b/internal/runner/module.go deleted file mode 100644 index 08289e2..0000000 --- a/internal/runner/module.go +++ /dev/null @@ -1,110 +0,0 @@ -package runner - -import ( - "fmt" - "os" - "path/filepath" - "strings" -) - -// resolveModuleDir returns the Go module root and adjusted goTestArgs for the -// package patterns in goTestArgs. When relative patterns (./foo/...) point into -// a subdirectory that owns its own go.mod, the working directory is moved to -// that submodule root and the patterns are rewritten relative to it. -// An error is returned when patterns span more than one module. -func resolveModuleDir(repoRoot string, goTestArgs []string) (string, []string, error) { - patterns := packagePatternsFromEnd(goTestArgs) - - var relPatterns []string - for _, p := range patterns { - if isRelativePackagePattern(p) { - relPatterns = append(relPatterns, p) - } - } - - if len(relPatterns) == 0 { - return repoRoot, goTestArgs, nil - } - - var moduleRoot string - for _, p := range relPatterns { - dir := patternBaseDir(p) - abs := filepath.Join(repoRoot, dir) - mod := nearestModuleRoot(abs, repoRoot) - if moduleRoot == "" { - moduleRoot = mod - } else if moduleRoot != mod { - return "", nil, fmt.Errorf( - "package patterns span multiple Go modules (%s and %s): run go test for each module separately", - moduleRoot, - mod, - ) - } - } - - if moduleRoot == repoRoot { - return repoRoot, goTestArgs, nil - } - - adjusted := rewriteRelativePatterns(repoRoot, moduleRoot, goTestArgs) - return moduleRoot, adjusted, nil -} - -func isRelativePackagePattern(p string) bool { - return strings.HasPrefix(p, "./") || strings.HasPrefix(p, "../") || p == "." || p == ".." -} - -// patternBaseDir strips trailing /... and /. wildcards to get the directory part. -func patternBaseDir(p string) string { - if s, ok := strings.CutSuffix(p, "/..."); ok { - return s - } - if s, ok := strings.CutSuffix(p, "/."); ok { - return s - } - return p -} - -// nearestModuleRoot walks up from dir toward stopAt looking for a go.mod. -// Returns stopAt if no intermediate go.mod is found. -func nearestModuleRoot(dir, stopAt string) string { - d := filepath.Clean(dir) - stop := filepath.Clean(stopAt) - for d != stop { - if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { - return d - } - parent := filepath.Dir(d) - if parent == d { - return stop - } - d = parent - } - return stop -} - -// rewriteRelativePatterns rewrites each relative package pattern in goTestArgs -// so it is expressed relative to moduleRoot instead of repoRoot. -func rewriteRelativePatterns(repoRoot, moduleRoot string, goTestArgs []string) []string { - result := make([]string, len(goTestArgs)) - for i, arg := range goTestArgs { - if !isRelativePackagePattern(arg) || !looksLikeGoPackagePattern(arg) { - result[i] = arg - continue - } - base := patternBaseDir(arg) - suffix := arg[len(base):] - abs := filepath.Join(repoRoot, base) - rel, err := filepath.Rel(moduleRoot, abs) - if err != nil { - result[i] = arg - continue - } - if rel == "." { - result[i] = "." + suffix - } else { - result[i] = "./" + rel + suffix - } - } - return result -} diff --git a/internal/runner/module_test.go b/internal/runner/module_test.go deleted file mode 100644 index 412a97a..0000000 --- a/internal/runner/module_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package runner - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func makeTestRepo(t *testing.T) string { - t.Helper() - root := t.TempDir() - // Root module - require.NoError( - t, - os.WriteFile(filepath.Join(root, "go.mod"), []byte("module github.com/smartcontractkit/chainlink/v2\n"), 0600), - ) - // core/ — plain subdirectory, no go.mod - require.NoError(t, os.MkdirAll(filepath.Join(root, "core", "store"), 0700)) - // deployment/ — separate module - require.NoError(t, os.MkdirAll(filepath.Join(root, "deployment", "ccip"), 0700)) - require.NoError( - t, - os.WriteFile( - filepath.Join(root, "deployment", "go.mod"), - []byte("module github.com/smartcontractkit/chainlink/deployment\n"), - 0600, - ), - ) - return root -} - -func TestResolveModuleDir(t *testing.T) { - t.Parallel() - root := makeTestRepo(t) - - tests := []struct { - name string - goTestArgs []string - wantDir string - wantArgs []string - wantErr bool - }{ - { - name: "core package stays at repo root", - goTestArgs: []string{"./core/..."}, - wantDir: root, - wantArgs: []string{"./core/..."}, - }, - { - name: "deployment top-level rewrites pattern to dot-slash-dot-dot-dot", - goTestArgs: []string{"./deployment/..."}, - wantDir: filepath.Join(root, "deployment"), - wantArgs: []string{"./..."}, - }, - { - name: "deployment subdirectory rewrites pattern relative to deployment root", - goTestArgs: []string{"./deployment/ccip/..."}, - wantDir: filepath.Join(root, "deployment"), - wantArgs: []string{"./ccip/..."}, - }, - { - name: "flags before pattern are preserved unchanged", - goTestArgs: []string{"-v", "-timeout=10m", "./deployment/..."}, - wantDir: filepath.Join(root, "deployment"), - wantArgs: []string{"-v", "-timeout=10m", "./..."}, - }, - { - name: "dot-slash-dot-dot-dot at repo root stays at repo root", - goTestArgs: []string{"./..."}, - wantDir: root, - wantArgs: []string{"./..."}, - }, - { - name: "no package patterns returns repo root unchanged", - goTestArgs: []string{"-v", "-count=1"}, - wantDir: root, - wantArgs: []string{"-v", "-count=1"}, - }, - { - name: "mixed core and deployment patterns error", - goTestArgs: []string{"./core/...", "./deployment/..."}, - wantErr: true, - }, - { - name: "specific deployment package without wildcard", - goTestArgs: []string{"./deployment/ccip"}, - wantDir: filepath.Join(root, "deployment"), - wantArgs: []string{"./ccip"}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - dir, args, err := resolveModuleDir(root, tc.goTestArgs) - if tc.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - require.Equal(t, tc.wantDir, dir) - require.Equal(t, tc.wantArgs, args) - }) - } -} diff --git a/internal/runner/runner.go b/internal/runner/runner.go index a70cf89..992d816 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -22,6 +22,7 @@ import ( "github.com/smartcontractkit/testrig/internal/hooks" "github.com/smartcontractkit/testrig/internal/output" "github.com/smartcontractkit/testrig/internal/termstyle" + "github.com/smartcontractkit/testrig/modresolve" ) // failFastReasonBuildFailure is used when go test reports a compile/build failure @@ -65,7 +66,7 @@ type diagnoseRunState struct { } func runCommand(ctx context.Context, conf *config.App, binary string, args []string, env []string) error { - dir, args, err := resolveModuleDir(conf.RepoRoot, args) + dir, args, err := modresolve.ResolveArgs(conf.RepoRoot, args) if err != nil { return err } @@ -410,7 +411,7 @@ func runDiagnoseIterations( hooks.runIteration = diagnoseIteration } - moduleDir, adjustedArgs, err := resolveModuleDir(conf.RepoRoot, goTestArgs) + moduleDir, adjustedArgs, err := modresolve.ResolveArgs(conf.RepoRoot, goTestArgs) if err != nil { return diagnoseRunState{}, err } @@ -683,23 +684,12 @@ func failFastDigestMatch(d IterationDigest, categories []string) (bool, string) return false, "" } -// goTestFlagsBeforeArgs returns the portion of argv that belongs to `go test` -// itself, stopping before -args (flags after -args are passed to the test binary). -func goTestFlagsBeforeArgs(args []string) []string { - for i, a := range args { - if a == "-args" { - return args[:i] - } - } - return args -} - // findLastFlagValue returns the last value of -flag (forms "-flag=val" and // "-flag val") within the go test flag section (before -args). set is false // when the flag does not appear. An error is returned only for "-flag" with no // following token. func findLastFlagValue(goTestArgs []string, flag string) (raw string, set bool, err error) { - args := goTestFlagsBeforeArgs(goTestArgs) + args := modresolve.GoTestFlagsBeforeArgs(goTestArgs) prefix := flag + "=" for i := 0; i < len(args); i++ { a := args[i] diff --git a/internal/runner/runner_bench_test.go b/internal/runner/runner_bench_test.go index 3ddd457..f304ad0 100644 --- a/internal/runner/runner_bench_test.go +++ b/internal/runner/runner_bench_test.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/testrig/internal/config" "github.com/smartcontractkit/testrig/internal/output" + "github.com/smartcontractkit/testrig/modresolve" ) const ( @@ -410,13 +411,13 @@ func TestOverheadMatrixRuns(t *testing.T) { require.Equal(t, overheadMatrixRunsDefault, overheadMatrixRuns()) } -func BenchmarkResolveModuleDir(b *testing.B) { +func BenchmarkResolveArgs(b *testing.B) { repoRoot, err := filepath.Abs("../..") require.NoError(b, err) args := []string{"./internal/runner/..."} for b.Loop() { - _, _, err := resolveModuleDir(repoRoot, args) + _, _, err := modresolve.ResolveArgs(repoRoot, args) require.NoError(b, err) } } diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index 9a16f63..a9587cf 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -28,7 +28,7 @@ var diagnoseResultsDirNameAt = time.Date(2024, 6, 1, 12, 30, 45, 0, time.UTC) // Ctrl+C'ing a long-running diagnose run. func TestDiagnoseCanceledCtxRunsNoIterationsButStillWritesReport(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) conf := &config.App{ RepoRoot: repoRoot, AIOutput: true, @@ -72,7 +72,7 @@ func TestDiagnoseCanceledCtxRunsNoIterationsButStillWritesReport(t *testing.T) { func TestDiagnoseCanceledCtxAIStdoutCompleteEvent(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) conf := &config.App{ RepoRoot: repoRoot, AIOutput: true, @@ -106,7 +106,7 @@ func TestDiagnoseCanceledCtxAIStdoutCompleteEvent(t *testing.T) { func TestDiagnoseHumanModeFooterShowsReportJSONPath(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) conf := &config.App{ RepoRoot: repoRoot, AIOutput: false, @@ -531,7 +531,7 @@ func TestBuildDiagnoseArgs(t *testing.T) { func TestDiagnoseShuffleSeedsAbsentWhenNoIterationsRun(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) conf := &config.App{ RepoRoot: repoRoot, AIOutput: true, @@ -638,7 +638,7 @@ func TestDiagnoseResultsDirNameLongRunAndPath(t *testing.T) { func TestMakeDiagnoseResultsDirAvoidsExistingDirectory(t *testing.T) { t.Parallel() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), Iterations: 1, } first, err := makeDiagnoseResultsDir(conf, []string{"./pkg"}, diagnoseResultsDirNameAt) @@ -654,7 +654,7 @@ func TestMakeDiagnoseResultsDirAvoidsExistingDirectory(t *testing.T) { func TestRunDiagnoseIterationsDumpDBSkippedWhenCancelled(t *testing.T) { t.Parallel() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: true, Iterations: 2, ParallelIterations: 1, @@ -721,23 +721,9 @@ func TestTruncateUTF8MaxBytes(t *testing.T) { assert.Equal(t, "abc\uFFFD", truncateUTF8MaxBytes("abc\uFFFD"+"x", 6)) } -func TestPackagePatternsFromEnd(t *testing.T) { - t.Parallel() - assert.Equal( - t, - []string{"./core/...", "./foo"}, - packagePatternsFromEnd([]string{"-race", "-timeout=5m", "./core/...", "./foo"}), - ) - assert.Nil(t, packagePatternsFromEnd([]string{"-v", "-race"})) - assert.Equal(t, []string{"./core/..."}, packagePatternsFromEnd([]string{"-timeout", "10m", "./core/..."})) - assert.Nil(t, packagePatternsFromEnd([]string{"-timeout", "10m"})) - assert.Equal(t, []string{"./pkg"}, packagePatternsFromEnd([]string{"./pkg", "-run", "TestName"})) - assert.Equal(t, []string{"./pkg"}, packagePatternsFromEnd([]string{"-run", "TestName", "./pkg"})) -} - func TestRunDiagnoseIterationsRunsInParallelWithWorkerIsolation(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) resultsDir := t.TempDir() conf := &config.App{ RepoRoot: repoRoot, @@ -836,7 +822,7 @@ func TestRunDiagnoseIterationsFailFastCancelsNewWork(t *testing.T) { t.Parallel() resultsDir := t.TempDir() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: true, Iterations: 5, ParallelIterations: 2, @@ -902,7 +888,7 @@ func TestRunDiagnoseIterationsStopsOnBuildFailure(t *testing.T) { t.Parallel() resultsDir := t.TempDir() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: true, Iterations: 5, // Build failure stops even without --fail-fast or --fail-fast-on. @@ -953,7 +939,7 @@ func TestRunDiagnoseIterations_serialLiveProgressMutex_noMergedProgressAndTableL resultsDir := t.TempDir() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: false, Iterations: 60, ParallelIterations: 1, @@ -1091,7 +1077,7 @@ func TestRunDiagnoseIterationsFailFastOnCategories(t *testing.T) { t.Parallel() resultsDir := t.TempDir() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: true, Iterations: 3, SlowThreshold: 30 * time.Second, @@ -1225,7 +1211,7 @@ func TestDiagnoseBuildErrorStopsWithoutAnalysis(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { t.Parallel() - repoRoot := t.TempDir() + repoRoot := makeTestRepoRoot(t) conf := &config.App{ RepoRoot: repoRoot, AIOutput: tc.aiOutput, @@ -1277,7 +1263,7 @@ func TestRunDiagnoseIterationsCallsIterationHooks(t *testing.T) { t.Parallel() resultsDir := t.TempDir() conf := &config.App{ - RepoRoot: t.TempDir(), + RepoRoot: makeTestRepoRoot(t), AIOutput: true, Iterations: 3, ParallelIterations: 1, @@ -1315,3 +1301,10 @@ func TestRunDiagnoseIterationsCallsIterationHooks(t *testing.T) { assert.Equal(t, int32(3), teardownCalls.Load(), "teardown called once per iteration") assert.True(t, setupBeforeTeardown.Load(), "setup must be called before teardown") } + +func makeTestRepoRoot(t *testing.T) string { + d := t.TempDir() + err := os.WriteFile(filepath.Join(d, "go.mod"), []byte("module test\n"), 0600) + require.NoError(t, err) + return d +} diff --git a/modresolve/modresolve.go b/modresolve/modresolve.go new file mode 100644 index 0000000..ec88f35 --- /dev/null +++ b/modresolve/modresolve.go @@ -0,0 +1,157 @@ +// Package modresolve resolves Go module roots for package patterns in multi-module +// repositories. Given relative patterns like ./deployment/..., it walks up from +// the pattern's base directory to find the nearest go.mod (bounded at repoRoot), +// rewrites patterns relative to that module, and returns the directory to use +// as cmd.Dir for go test or go list. +package modresolve + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// ResolvePatterns returns the Go module root for relative package patterns and +// rewrites those patterns relative to the module directory. When all patterns +// belong to repoRoot's module, repoRoot is returned with patterns unchanged. +// Returns an error when patterns span more than one module. +func ResolvePatterns(repoRoot string, patterns []string) (moduleDir string, rewritten []string, err error) { + repoRoot = filepath.Clean(repoRoot) + moduleDir, err = resolveModuleFromPatterns(repoRoot, patterns) + if err != nil { + return "", nil, err + } + if moduleDir == repoRoot { + return repoRoot, patterns, nil + } + return moduleDir, rewritePatterns(repoRoot, moduleDir, patterns), nil +} + +// ResolveArgs extracts trailing package patterns from go test arguments (see +// PackagePatternsFromEnd), resolves the module directory from those patterns, +// and rewrites all relative package-pattern arguments in the full slice. Module +// resolution uses only trailing patterns; rewriting applies to every relative +// pattern argument, not only trailing ones. Flags and non-pattern args are preserved. +func ResolveArgs(repoRoot string, goTestArgs []string) (moduleDir string, rewrittenArgs []string, err error) { + repoRoot = filepath.Clean(repoRoot) + patterns := PackagePatternsFromEnd(goTestArgs) + + moduleDir, err = resolveModuleFromPatterns(repoRoot, patterns) + if err != nil { + return "", nil, err + } + if moduleDir == repoRoot { + return repoRoot, goTestArgs, nil + } + return moduleDir, rewriteRelativePatterns(repoRoot, moduleDir, goTestArgs), nil +} + +// NearestModuleRoot walks up from dir toward stopAt looking for a go.mod. +// Returns an error if no intermediate or stopAt go.mod is found. +func NearestModuleRoot(dir, stopAt string) (string, error) { + return nearestModuleRoot(dir, stopAt) +} + +func resolveModuleFromPatterns(repoRoot string, patterns []string) (moduleDir string, err error) { + var relative []string + for _, p := range patterns { + if isRelativePackagePattern(p) { + relative = append(relative, p) + } + } + if len(relative) == 0 { + return repoRoot, nil + } + + var moduleRoot string + for _, p := range relative { + dir := patternBaseDir(p) + abs := filepath.Join(repoRoot, dir) + mod, err := nearestModuleRoot(abs, repoRoot) + if err != nil { + return "", err + } + if moduleRoot == "" { + moduleRoot = mod + } else if moduleRoot != mod { + return "", fmt.Errorf( + "package patterns span multiple Go modules (%s and %s): run go test for each module separately", + moduleRoot, mod, + ) + } + } + return moduleRoot, nil +} + +func isRelativePackagePattern(p string) bool { + return strings.HasPrefix(p, "./") || strings.HasPrefix(p, "../") || p == "." || p == ".." +} + +func patternBaseDir(p string) string { + if s, ok := strings.CutSuffix(p, "/..."); ok { + return s + } + if s, ok := strings.CutSuffix(p, "/."); ok { + return s + } + return p +} + +func nearestModuleRoot(dir, stopAt string) (string, error) { + d := filepath.Clean(dir) + stop := filepath.Clean(stopAt) + for { + if _, err := os.Stat(filepath.Join(d, "go.mod")); err == nil { + return d, nil + } + if d == stop { + break + } + parent := filepath.Dir(d) + if parent == d { + break + } + d = parent + } + return "", fmt.Errorf("no go.mod found between %s and %s", dir, stopAt) +} + +func rewriteOneRelativePattern(repoRoot, moduleDir, p string) string { + base := patternBaseDir(p) + suffix := p[len(base):] + abs := filepath.Join(repoRoot, base) + rel, err := filepath.Rel(moduleDir, abs) + if err != nil { + return p + } + rel = filepath.ToSlash(rel) + if rel == "." { + return "." + suffix + } + return "./" + rel + suffix +} + +func rewritePatterns(repoRoot, moduleDir string, patterns []string) []string { + result := make([]string, len(patterns)) + for i, p := range patterns { + if !isRelativePackagePattern(p) { + result[i] = p + continue + } + result[i] = rewriteOneRelativePattern(repoRoot, moduleDir, p) + } + return result +} + +func rewriteRelativePatterns(repoRoot, moduleDir string, goTestArgs []string) []string { + result := make([]string, len(goTestArgs)) + for i, arg := range goTestArgs { + if !isRelativePackagePattern(arg) || !looksLikeGoPackagePattern(arg) { + result[i] = arg + continue + } + result[i] = rewriteOneRelativePattern(repoRoot, moduleDir, arg) + } + return result +} diff --git a/modresolve/modresolve_test.go b/modresolve/modresolve_test.go new file mode 100644 index 0000000..12a2d54 --- /dev/null +++ b/modresolve/modresolve_test.go @@ -0,0 +1,239 @@ +package modresolve_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/testrig/modresolve" +) + +func makeTestRepo(t *testing.T) string { + t.Helper() + root := t.TempDir() + require.NoError(t, os.WriteFile( + filepath.Join(root, "go.mod"), + []byte("module github.com/smartcontractkit/chainlink/v2\n"), + 0600, + )) + require.NoError(t, os.MkdirAll(filepath.Join(root, "core", "store"), 0700)) + require.NoError(t, os.MkdirAll(filepath.Join(root, "deployment", "ccip"), 0700)) + require.NoError(t, os.WriteFile( + filepath.Join(root, "deployment", "go.mod"), + []byte("module github.com/smartcontractkit/chainlink/deployment\n"), + 0600, + )) + return root +} + +func TestResolveArgs(t *testing.T) { + t.Parallel() + root := makeTestRepo(t) + + tests := []struct { + name string + goTestArgs []string + wantDir string + wantArgs []string + wantErr bool + }{ + { + name: "core package stays at repo root", + goTestArgs: []string{"./core/..."}, + wantDir: root, + wantArgs: []string{"./core/..."}, + }, + { + name: "deployment top-level rewrites pattern to dot-slash-dot-dot-dot", + goTestArgs: []string{"./deployment/..."}, + wantDir: filepath.Join(root, "deployment"), + wantArgs: []string{"./..."}, + }, + { + name: "deployment subdirectory rewrites pattern relative to deployment root", + goTestArgs: []string{"./deployment/ccip/..."}, + wantDir: filepath.Join(root, "deployment"), + wantArgs: []string{"./ccip/..."}, + }, + { + name: "flags before pattern are preserved unchanged", + goTestArgs: []string{"-v", "-timeout=10m", "./deployment/..."}, + wantDir: filepath.Join(root, "deployment"), + wantArgs: []string{"-v", "-timeout=10m", "./..."}, + }, + { + name: "dot-slash-dot-dot-dot at repo root stays at repo root", + goTestArgs: []string{"./..."}, + wantDir: root, + wantArgs: []string{"./..."}, + }, + { + name: "no package patterns returns repo root unchanged", + goTestArgs: []string{"-v", "-count=1"}, + wantDir: root, + wantArgs: []string{"-v", "-count=1"}, + }, + { + name: "mixed core and deployment patterns error", + goTestArgs: []string{"./core/...", "./deployment/..."}, + wantErr: true, + }, + { + name: "specific deployment package without wildcard", + goTestArgs: []string{"./deployment/ccip"}, + wantDir: filepath.Join(root, "deployment"), + wantArgs: []string{"./ccip"}, + }, + { + name: "import path only stays at repo root", + goTestArgs: []string{"-v", "github.com/smartcontractkit/chainlink/deployment/..."}, + wantDir: root, + wantArgs: []string{"-v", "github.com/smartcontractkit/chainlink/deployment/..."}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + dir, args, err := modresolve.ResolveArgs(root, tc.goTestArgs) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.wantDir, dir) + require.Equal(t, tc.wantArgs, args) + }) + } +} + +func TestResolvePatterns(t *testing.T) { + t.Parallel() + root := makeTestRepo(t) + depDir := filepath.Join(root, "deployment") + + tests := []struct { + name string + patterns []string + wantDir string + wantPats []string + wantErr bool + }{ + { + name: "core stays at root", + patterns: []string{"./core/..."}, + wantDir: root, + wantPats: []string{"./core/..."}, + }, + { + name: "deployment top-level rewrites", + patterns: []string{"./deployment/..."}, + wantDir: depDir, + wantPats: []string{"./..."}, + }, + { + name: "deployment subpackage rewrites", + patterns: []string{"./deployment/ccip/..."}, + wantDir: depDir, + wantPats: []string{"./ccip/..."}, + }, + { + name: "cross-module error", + patterns: []string{"./core/...", "./deployment/..."}, + wantErr: true, + }, + { + name: "import path only stays at repo root", + patterns: []string{"github.com/smartcontractkit/chainlink/deployment/..."}, + wantDir: root, + wantPats: []string{"github.com/smartcontractkit/chainlink/deployment/..."}, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + dir, pats, err := modresolve.ResolvePatterns(root, tc.patterns) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.wantDir, dir) + require.Equal(t, tc.wantPats, pats) + }) + } +} + +func TestNearestModuleRoot(t *testing.T) { + t.Parallel() + root := makeTestRepo(t) + + tests := []struct { + name string + dir string + wantDir string + }{ + {name: "repo root", dir: root, wantDir: root}, + {name: "core no intermediate go.mod", dir: filepath.Join(root, "core", "store"), wantDir: root}, + {name: "deployment dir", dir: filepath.Join(root, "deployment"), wantDir: filepath.Join(root, "deployment")}, + { + name: "deployment subdir", + dir: filepath.Join(root, "deployment", "ccip"), + wantDir: filepath.Join(root, "deployment"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := modresolve.NearestModuleRoot(tc.dir, root) + require.NoError(t, err) + require.Equal(t, tc.wantDir, got) + }) + } +} + +func TestNearestModuleRootNoGoMod(t *testing.T) { + t.Parallel() + root := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(root, "core"), 0700)) + + _, err := modresolve.NearestModuleRoot(filepath.Join(root, "core"), root) + require.Error(t, err) + require.Contains(t, err.Error(), "no go.mod found") +} + +func TestResolveArgsNoGoMod(t *testing.T) { + t.Parallel() + root := t.TempDir() + + _, _, err := modresolve.ResolveArgs(root, []string{"./core/..."}) + require.Error(t, err) + require.Contains(t, err.Error(), "no go.mod found") +} + +func TestResolvePatternsNoGoMod(t *testing.T) { + t.Parallel() + root := t.TempDir() + + _, _, err := modresolve.ResolvePatterns(root, []string{"./core/..."}) + require.Error(t, err) + require.Contains(t, err.Error(), "no go.mod found") +} + +func TestResolveArgsRewrittenPatternsUseForwardSlashes(t *testing.T) { + t.Parallel() + root := makeTestRepo(t) + + _, args, err := modresolve.ResolveArgs(root, []string{"./deployment/ccip/..."}) + require.NoError(t, err) + for _, arg := range args { + if strings.Contains(arg, "/") || strings.HasPrefix(arg, "./") { + require.NotContains(t, arg, `\`, "pattern %q should use forward slashes", arg) + } + } +} diff --git a/modresolve/patterns.go b/modresolve/patterns.go new file mode 100644 index 0000000..d758858 --- /dev/null +++ b/modresolve/patterns.go @@ -0,0 +1,83 @@ +package modresolve + +import ( + "slices" + "strings" +) + +// testFilterTwoArgSuffixFlags are go test filter flags that consume the following +// argv token when scanning backward for package patterns. +var testFilterTwoArgSuffixFlags = map[string]bool{ + "-run": true, + "-bench": true, + "-skip": true, + "-fuzz": true, +} + +func singleArgTestBinaryFlagPrefix(arg string) (prefix string, ok bool) { + for _, p := range []string{"-run=", "-bench=", "-skip=", "-fuzz="} { + if strings.HasPrefix(arg, p) { + return p, true + } + } + return "", false +} + +func looksLikeGoPackagePattern(arg string) bool { + return strings.Contains(arg, ".") || + strings.Contains(arg, "/") || + strings.Contains(arg, "...") +} + +// GoTestFlagsBeforeArgs returns the portion of argv that belongs to `go test` +// itself, stopping before -args (flags after -args are passed to the test binary). +func GoTestFlagsBeforeArgs(args []string) []string { + for i, a := range args { + if a == "-args" { + return args[:i] + } + } + return args +} + +// PackagePatternsFromEnd returns trailing arguments that look like package patterns. +// It scans backward from the end of GoTestFlagsBeforeArgs(args), skipping standard +// test binary flags and their values so `./pkg -run TestName` still yields `./pkg`. +func PackagePatternsFromEnd(args []string) []string { + args = GoTestFlagsBeforeArgs(args) + var pkgs []string + inPkgs := false + for i := len(args) - 1; i >= 0; i-- { + arg := args[i] + if _, ok := singleArgTestBinaryFlagPrefix(arg); ok { + if inPkgs { + break + } + continue + } + if i >= 1 && testFilterTwoArgSuffixFlags[args[i-1]] { + if inPkgs { + break + } + i-- + continue + } + isFlag := strings.HasPrefix(arg, "-") + if isFlag { + if inPkgs { + break + } + continue + } + if !looksLikeGoPackagePattern(arg) { + if inPkgs { + break + } + continue + } + inPkgs = true + pkgs = append(pkgs, arg) + } + slices.Reverse(pkgs) + return pkgs +} diff --git a/modresolve/patterns_test.go b/modresolve/patterns_test.go new file mode 100644 index 0000000..4fb0b6b --- /dev/null +++ b/modresolve/patterns_test.go @@ -0,0 +1,81 @@ +package modresolve_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/testrig/modresolve" +) + +func TestGoTestFlagsBeforeArgs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + args []string + want []string + }{ + { + name: "no -args returns all", + args: []string{"-v", "./pkg"}, + want: []string{"-v", "./pkg"}, + }, + { + name: "stops before -args", + args: []string{"-v", "./pkg", "-args", "-test.v"}, + want: []string{"-v", "./pkg"}, + }, + { + name: "empty", + args: nil, + want: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + assert.Equal(t, tc.want, modresolve.GoTestFlagsBeforeArgs(tc.args)) + }) + } +} + +func TestPackagePatternsFromEnd(t *testing.T) { + t.Parallel() + + assert.Equal(t, + []string{"./core/...", "./foo"}, + modresolve.PackagePatternsFromEnd([]string{"-race", "-timeout=5m", "./core/...", "./foo"}), + ) + assert.Nil(t, modresolve.PackagePatternsFromEnd([]string{"-v", "-race"})) + assert.Equal(t, + []string{"./core/..."}, + modresolve.PackagePatternsFromEnd([]string{"-timeout", "10m", "./core/..."}), + ) + assert.Nil(t, modresolve.PackagePatternsFromEnd([]string{"-timeout", "10m"})) + assert.Equal(t, + []string{"./pkg"}, + modresolve.PackagePatternsFromEnd([]string{"./pkg", "-run", "TestName"}), + ) + assert.Equal(t, + []string{"./pkg"}, + modresolve.PackagePatternsFromEnd([]string{"-run", "TestName", "./pkg"}), + ) + assert.Equal(t, + []string{"./pkg"}, + modresolve.PackagePatternsFromEnd([]string{"./pkg", "-count=1"}), + ) + assert.Equal(t, + []string{"./pkg"}, + modresolve.PackagePatternsFromEnd([]string{"./pkg", "-count", "1"}), + ) + assert.Equal(t, + []string{"github.com/foo/bar/..."}, + modresolve.PackagePatternsFromEnd([]string{"-v", "github.com/foo/bar/..."}), + ) + assert.Equal(t, + []string{"./pkg"}, + modresolve.PackagePatternsFromEnd([]string{"./pkg", "-run", "TestName", "-count=1"}), + ) +} diff --git a/tools/test/go.mod b/tools/test/go.mod index 5032b2c..553f779 100644 --- a/tools/test/go.mod +++ b/tools/test/go.mod @@ -10,9 +10,9 @@ require ( charm.land/fang/v2 v2.0.1 // indirect charm.land/lipgloss/v2 v2.0.3 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa // indirect github.com/charmbracelet/x/ansi v0.11.7 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a // indirect + github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect @@ -30,8 +30,8 @@ require ( github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect + golang.org/x/text v0.38.0 // indirect ) diff --git a/tools/test/go.sum b/tools/test/go.sum index 54a0e52..5452d84 100644 --- a/tools/test/go.sum +++ b/tools/test/go.sum @@ -6,12 +6,12 @@ github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f h1:vKsPSlO4g4jKfJ9enESgNZ45BkbHngTIq3UxNOzic74= -github.com/charmbracelet/ultraviolet v0.0.0-20260601155805-6cf7526a1b3f/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= +github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa h1:rRT2qwk9xbontVloCXEUIsl1ePz0XFcIWkGi2bvmSTY= +github.com/charmbracelet/ultraviolet v0.0.0-20260608091853-35bcb7319efa/go.mod h1:hFpumms29Smx3LStRfku8vcCTBe1Kq8aCXtHUJa3mjY= github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI= github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a h1:aVvnksCVgxB2igk7jERL9ARIkbDXccp1gXCFqhGlamQ= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20260602025833-85a30b5e440a/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5 h1:Xl3+pllTbd0iZWeTQixTHClROwU/Gs79ANuOGILkA5g= +github.com/charmbracelet/x/exp/charmtone v0.0.0-20260608090822-c3ad58c6c9e5/go.mod h1:nsExn0DGyX0lh9LwLHTn2Gg+hafdzfSXnC+QmEJTZFY= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= @@ -60,14 +60,14 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJu go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=