--- /dev/null
+// Copyright 2022 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package scripttest adapts the script engine for use in tests.
+package scripttest
+
+import (
+ "bytes"
+ "cmd/internal/script"
+ "context"
+ "fmt"
+ "internal/testenv"
+ "internal/txtar"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+)
+
+// ToolReplacement records the name of a tool to replace
+// within a given GOROOT for script testing purposes.
+type ToolReplacement struct {
+ ToolName string // e.g. compile, link, addr2line, etc
+ ReplacementPath string // path to replacement tool exe
+ EnvVar string // env var setting (e.g. "FOO=BAR")
+}
+
+// RunToolScriptTest kicks off a set of script tests runs for
+// a tool of some sort (compiler, linker, etc). The expectation
+// is that we'll be called from the top level cmd/X dir for tool X,
+// and that instead of executing the install tool X we'll use the
+// test binary instead.
+func RunToolScriptTest(t *testing.T, repls []ToolReplacement, pattern string) {
+ // Nearly all script tests involve doing builds, so don't
+ // bother here if we don't have "go build".
+ testenv.MustHaveGoBuild(t)
+
+ // Skip this path on plan9, which doesn't support symbolic
+ // links (we would have to copy too much).
+ if runtime.GOOS == "plan9" {
+ t.Skipf("no symlinks on plan9")
+ }
+
+ // Locate our Go tool.
+ gotool, err := testenv.GoTool()
+ if err != nil {
+ t.Fatalf("locating go tool: %v", err)
+ }
+
+ goEnv := func(name string) string {
+ out, err := exec.Command(gotool, "env", name).CombinedOutput()
+ if err != nil {
+ t.Fatalf("go env %s: %v\n%s", name, err, out)
+ }
+ return strings.TrimSpace(string(out))
+ }
+
+ // Construct an initial set of commands + conditions to make available
+ // to the script tests.
+ cmds := DefaultCmds()
+ conds := DefaultConds()
+
+ addcmd := func(name string, cmd script.Cmd) {
+ if _, ok := cmds[name]; ok {
+ panic(fmt.Sprintf("command %q is already registered", name))
+ }
+ cmds[name] = cmd
+ }
+
+ addcond := func(name string, cond script.Cond) {
+ if _, ok := conds[name]; ok {
+ panic(fmt.Sprintf("condition %q is already registered", name))
+ }
+ conds[name] = cond
+ }
+
+ prependToPath := func(env []string, dir string) {
+ found := false
+ for k := range env {
+ ev := env[k]
+ if !strings.HasPrefix(ev, "PATH=") {
+ continue
+ }
+ oldpath := ev[5:]
+ env[k] = "PATH=" + dir + string(filepath.ListSeparator) + oldpath
+ found = true
+ break
+ }
+ if !found {
+ t.Fatalf("could not update PATH")
+ }
+ }
+
+ setenv := func(env []string, varname, val string) []string {
+ pref := varname + "="
+ found := false
+ for k := range env {
+ if !strings.HasPrefix(env[k], pref) {
+ continue
+ }
+ env[k] = pref + val
+ found = true
+ break
+ }
+ if !found {
+ env = append(env, varname+"="+val)
+ }
+ return env
+ }
+
+ interrupt := func(cmd *exec.Cmd) error {
+ return cmd.Process.Signal(os.Interrupt)
+ }
+ gracePeriod := 60 * time.Second // arbitrary
+
+ // Set up an alternate go root for running script tests, since it
+ // is possible that we might want to replace one of the installed
+ // tools with a unit test executable.
+ goroot := goEnv("GOROOT")
+ tmpdir := t.TempDir()
+ tgr := SetupTestGoRoot(t, tmpdir, goroot)
+
+ // Replace tools if appropriate
+ for _, repl := range repls {
+ ReplaceGoToolInTestGoRoot(t, tgr, repl.ToolName, repl.ReplacementPath)
+ }
+
+ // Add in commands for "go" and "cc".
+ testgo := filepath.Join(tgr, "bin", "go")
+ gocmd := script.Program(testgo, interrupt, gracePeriod)
+ cccmd := script.Program(goEnv("CC"), interrupt, gracePeriod)
+ addcmd("go", gocmd)
+ addcmd("cc", cccmd)
+ addcond("cgo", script.BoolCondition("host CGO_ENABLED", testenv.HasCGO()))
+
+ // Environment setup.
+ env := os.Environ()
+ prependToPath(env, filepath.Join(tgr, "bin"))
+ env = setenv(env, "GOROOT", tgr)
+ for _, repl := range repls {
+ // consistency check
+ chunks := strings.Split(repl.EnvVar, "=")
+ if len(chunks) != 2 {
+ t.Fatalf("malformed env var setting: %s", repl.EnvVar)
+ }
+ env = append(env, repl.EnvVar)
+ }
+
+ // Manufacture engine...
+ engine := &script.Engine{
+ Conds: conds,
+ Cmds: cmds,
+ Quiet: !testing.Verbose(),
+ }
+
+ // ... and kick off tests.
+ ctx := context.Background()
+ RunTests(t, ctx, engine, env, pattern)
+}
+
+// RunTests kicks off one or more script-based tests using the
+// specified engine, running all test files that match pattern.
+// This function adapted from Russ's rsc.io/script/scripttest#Run
+// function, which was in turn forked off cmd/go's runner.
+func RunTests(t *testing.T, ctx context.Context, engine *script.Engine, env []string, pattern string) {
+ gracePeriod := 100 * time.Millisecond
+ if deadline, ok := t.Deadline(); ok {
+ timeout := time.Until(deadline)
+
+ // If time allows, increase the termination grace period to 5% of the
+ // remaining time.
+ if gp := timeout / 20; gp > gracePeriod {
+ gracePeriod = gp
+ }
+
+ // When we run commands that execute subprocesses, we want to
+ // reserve two grace periods to clean up. We will send the
+ // first termination signal when the context expires, then
+ // wait one grace period for the process to produce whatever
+ // useful output it can (such as a stack trace). After the
+ // first grace period expires, we'll escalate to os.Kill,
+ // leaving the second grace period for the test function to
+ // record its output before the test process itself
+ // terminates.
+ timeout -= 2 * gracePeriod
+
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithTimeout(ctx, timeout)
+ t.Cleanup(cancel)
+ }
+
+ files, _ := filepath.Glob(pattern)
+ if len(files) == 0 {
+ t.Fatal("no testdata")
+ }
+ for _, file := range files {
+ file := file
+ name := strings.TrimSuffix(filepath.Base(file), ".txt")
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+
+ workdir := t.TempDir()
+ s, err := script.NewState(ctx, workdir, env)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Unpack archive.
+ a, err := txtar.ParseFile(file)
+ if err != nil {
+ t.Fatal(err)
+ }
+ initScriptDirs(t, s)
+ if err := s.ExtractFiles(a); err != nil {
+ t.Fatal(err)
+ }
+
+ t.Log(time.Now().UTC().Format(time.RFC3339))
+ work, _ := s.LookupEnv("WORK")
+ t.Logf("$WORK=%s", work)
+
+ // Note: Do not use filepath.Base(file) here:
+ // editors that can jump to file:line references in the output
+ // will work better seeing the full path relative to the
+ // directory containing the command being tested
+ // (e.g. where "go test" command is usually run).
+ Run(t, engine, s, file, bytes.NewReader(a.Comment))
+ })
+ }
+}
+
+func initScriptDirs(t testing.TB, s *script.State) {
+ must := func(err error) {
+ if err != nil {
+ t.Helper()
+ t.Fatal(err)
+ }
+ }
+
+ work := s.Getwd()
+ must(s.Setenv("WORK", work))
+ must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
+ must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
+}
+
+func tempEnvName() string {
+ switch runtime.GOOS {
+ case "windows":
+ return "TMP"
+ case "plan9":
+ return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
+ default:
+ return "TMPDIR"
+ }
+}
--- /dev/null
+// Copyright 2024 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package scripttest adapts the script engine for use in tests.
+package scripttest
+
+import (
+ "io"
+ "os"
+ "path/filepath"
+ "runtime"
+ "testing"
+)
+
+// SetupTestGoRoot sets up a temporary GOROOT for use with script test
+// execution. It copies the existing goroot bin and pkg dirs using
+// symlinks (if possible) or raw copying. Return value is the path to
+// the newly created testgoroot dir.
+func SetupTestGoRoot(t *testing.T, tmpdir string, goroot string) string {
+ mustMkdir := func(path string) {
+ if err := os.MkdirAll(path, 0777); err != nil {
+ t.Fatalf("SetupTestGoRoot mkdir %s failed: %v", path, err)
+ }
+ }
+
+ replicateDir := func(srcdir, dstdir string) {
+ files, err := os.ReadDir(srcdir)
+ if err != nil {
+ t.Fatalf("inspecting %s: %v", srcdir, err)
+ }
+ for _, file := range files {
+ fn := file.Name()
+ linkOrCopy(t, filepath.Join(srcdir, fn), filepath.Join(dstdir, fn))
+ }
+ }
+
+ // Create various dirs in testgoroot.
+ toolsub := filepath.Join("tool", runtime.GOOS+"_"+runtime.GOARCH)
+ tomake := []string{
+ "bin",
+ "src",
+ "pkg",
+ filepath.Join("pkg", "include"),
+ filepath.Join("pkg", toolsub),
+ }
+ made := []string{}
+ tgr := filepath.Join(tmpdir, "testgoroot")
+ mustMkdir(tgr)
+ for _, targ := range tomake {
+ path := filepath.Join(tgr, targ)
+ mustMkdir(path)
+ made = append(made, path)
+ }
+
+ // Replicate selected portions of the content.
+ replicateDir(filepath.Join(goroot, "bin"), made[0])
+ replicateDir(filepath.Join(goroot, "src"), made[1])
+ replicateDir(filepath.Join(goroot, "pkg", "include"), made[3])
+ replicateDir(filepath.Join(goroot, "pkg", toolsub), made[4])
+
+ return tgr
+}
+
+// ReplaceGoToolInTestGoRoot replaces the go tool binary toolname with
+// an alternate executable newtoolpath within a test GOROOT directory
+// previously created by SetupTestGoRoot.
+func ReplaceGoToolInTestGoRoot(t *testing.T, testgoroot, toolname, newtoolpath string) {
+ toolsub := filepath.Join("pkg", "tool", runtime.GOOS+"_"+runtime.GOARCH)
+ exename := toolname
+ if runtime.GOOS == "windows" {
+ exename += ".exe"
+ }
+ toolpath := filepath.Join(testgoroot, toolsub, exename)
+ if err := os.Remove(toolpath); err != nil {
+ t.Fatalf("removing %s: %v", toolpath, err)
+ }
+ linkOrCopy(t, newtoolpath, toolpath)
+}
+
+// linkOrCopy creates a link to src at dst, or if the symlink fails
+// (platform doesn't support) then copies src to dst.
+func linkOrCopy(t *testing.T, src, dst string) {
+ err := os.Symlink(src, dst)
+ if err == nil {
+ return
+ }
+ srcf, err := os.Open(src)
+ if err != nil {
+ t.Fatalf("copying %s to %s: %v", src, dst, err)
+ }
+ defer srcf.Close()
+ perm := os.O_WRONLY | os.O_CREATE | os.O_EXCL
+ dstf, err := os.OpenFile(dst, perm, 0o777)
+ if err != nil {
+ t.Fatalf("copying %s to %s: %v", src, dst, err)
+ }
+ _, err = io.Copy(dstf, srcf)
+ if closeErr := dstf.Close(); err == nil {
+ err = closeErr
+ }
+ if err != nil {
+ t.Fatalf("copying %s to %s: %v", src, dst, err)
+ }
+}