aboutsummaryrefslogtreecommitdiff
path: root/src/cmd/internal/script/scripttest/run.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/cmd/internal/script/scripttest/run.go')
-rw-r--r--src/cmd/internal/script/scripttest/run.go259
1 files changed, 259 insertions, 0 deletions
diff --git a/src/cmd/internal/script/scripttest/run.go b/src/cmd/internal/script/scripttest/run.go
new file mode 100644
index 0000000000..d2f3ed8ca9
--- /dev/null
+++ b/src/cmd/internal/script/scripttest/run.go
@@ -0,0 +1,259 @@
+// 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"
+ }
+}