| // 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, scriptsdir string, fixReadme bool) { |
| // 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 |
| } |
| |
| 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) |
| addcmd("go", gocmd) |
| cmdExec := cmds["exec"] |
| addcmd("cc", scriptCC(cmdExec, goEnv("CC"))) |
| |
| // Add various helpful conditions related to builds and toolchain use. |
| goHostOS, goHostArch := goEnv("GOHOSTOS"), goEnv("GOHOSTARCH") |
| AddToolChainScriptConditions(t, conds, goHostOS, goHostArch) |
| |
| // Environment setup. |
| env := os.Environ() |
| prependToPath(env, filepath.Join(tgr, "bin")) |
| env = setenv(env, "GOROOT", tgr) |
| // GOOS and GOARCH are expected to be set by the toolchain script conditions. |
| env = setenv(env, "GOOS", runtime.GOOS) |
| env = setenv(env, "GOARCH", runtime.GOARCH) |
| 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(), |
| } |
| |
| t.Run("README", func(t *testing.T) { |
| checkScriptReadme(t, engine, env, scriptsdir, gotool, fixReadme) |
| }) |
| |
| // ... and kick off tests. |
| ctx := context.Background() |
| pattern := filepath.Join(scriptsdir, "*.txt") |
| 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" |
| } |
| } |
| |
| // scriptCC runs the platform C compiler. |
| func scriptCC(cmdExec script.Cmd, ccexe string) script.Cmd { |
| return script.Command( |
| script.CmdUsage{ |
| Summary: "run the platform C compiler", |
| Args: "args...", |
| }, |
| func(s *script.State, args ...string) (script.WaitFunc, error) { |
| return cmdExec.Run(s, append([]string{ccexe}, args...)...) |
| }) |
| } |