diff options
Diffstat (limited to 'src/cmd/internal/script/scripttest/run.go')
| -rw-r--r-- | src/cmd/internal/script/scripttest/run.go | 259 |
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" + } +} |
