diff --git a/go.mod b/go.mod index 01ce72e..d470c94 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,13 @@ module github.com/rogpeppe/go-internal -go 1.23 +go 1.23.0 require ( - golang.org/x/mod v0.21.0 - golang.org/x/sys v0.26.0 - golang.org/x/tools v0.26.0 + golang.org/x/mod v0.26.0 + golang.org/x/sys v0.34.0 + golang.org/x/tools v0.34.0 ) + +require golang.org/x/sync v0.15.0 // indirect + +tool golang.org/x/tools/cmd/stringer diff --git a/go.sum b/go.sum index 8693c9d..ebc6069 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= diff --git a/testscript/exe.go b/testscript/exe.go index 7e6983b..8e76442 100644 --- a/testscript/exe.go +++ b/testscript/exe.go @@ -5,9 +5,12 @@ package testscript import ( + "bytes" + "fmt" "io" "log" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -58,6 +61,49 @@ func Main(m TestingM, commands map[string]func()) { os.Exit(0) } +// GoTool exposes a Go program added to the module being tested via `go get -tool`, +// which can then be run via `go tool $name` and leverage Go's module and build caches. +// This function must be run as part of [Main]; for example, after setting up the tool +// via `go get -tool golang.org/x/tools/cmd/stringer`: +// +// testscript.Main(m, map[string]func(){ +// "stringer": testscript.GoTool("stringer"), +// }) +func GoTool(name string) func() { + // Since [Main] only takes a map[string]func() as a parameter, we cannot store the path + // to the cached tool anywhere, so we resort to setting one env var per tool. + // This is not ideal, but it works. + // + // We could also directly copy the cached tool binary into the $PATH that testscript sets up, + // to avoid an indirection via the test binary to use os/exec below. + // However, this again is very difficult given the current API of [Main]. + // + // TODO: rethink in a future iteration of the API. + envName := "TESTSCRIPT_GO_TOOL_" + name + cachedBin := os.Getenv(envName) + if cachedBin == "" { + cmd := exec.Command("go", "tool", "-n", name) + out, err := cmd.CombinedOutput() + if err != nil { + log.Fatalf("failed to run %v: %v\n%s", strings.Join(cmd.Args, " "), err, out) + } + os.Setenv(envName, string(bytes.TrimSpace(out))) + } + return func() { + cmd := exec.Command(cachedBin, os.Args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if err, ok := err.(*exec.ExitError); ok { + os.Exit(err.ExitCode()) + } + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } +} + // testingMRun exists just so that we can use `defer`, given that [Main] above uses [os.Exit]. func testingMRun(m TestingM, commands map[string]func()) int { // Set up all commands in a directory, added in $PATH. diff --git a/testscript/testdata/go_tool.txtar b/testscript/testdata/go_tool.txtar new file mode 100644 index 0000000..b58e7eb --- /dev/null +++ b/testscript/testdata/go_tool.txtar @@ -0,0 +1,20 @@ +# We test the integration with `go tool X` with stringer, because it's contained +# by an existing dependency of ours in the form of x/tools. +# Moreover, it's a fairly simple tool to use, and given that it takes a relative +# path as an argument, it will catch whether the current directory is correct. + +env GOCACHE=${WORK}/.gocache + +stringer -type Foo foo.go +exists foo_string.go + +-- foo.go -- +package foo + +type Foo int + +const ( + _ Foo = iota + Foo1 + Foo2 +) diff --git a/testscript/testscript.go b/testscript/testscript.go index 4b790e8..83eb943 100644 --- a/testscript/testscript.go +++ b/testscript/testscript.go @@ -494,6 +494,12 @@ func (ts *TestScript) setup() string { env.Vars = append(env.Vars, name+"="+val) } } + // For [GoTool] to work, we must pass its env vars through. + for _, kv := range os.Environ() { + if strings.HasPrefix(kv, "TESTSCRIPT_GO_TOOL_") { + env.Vars = append(env.Vars, kv) + } + } // Must preserve SYSTEMROOT on Windows: https://github.com/golang/go/issues/25513 et al if runtime.GOOS == "windows" { env.Vars = append(env.Vars, diff --git a/testscript/testscript_test.go b/testscript/testscript_test.go index b2fea5d..82585ca 100644 --- a/testscript/testscript_test.go +++ b/testscript/testscript_test.go @@ -81,6 +81,7 @@ func TestMain(m *testing.M) { "status": exitWithStatus, "signalcatcher": signalCatcher, "terminalprompt": terminalPrompt, + "stringer": GoTool("stringer"), }) }