Source file src/cmd/internal/script/scripttest/run.go

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package scripttest adapts the script engine for use in tests.
     6  package scripttest
     7  
     8  import (
     9  	"bytes"
    10  	"cmd/internal/script"
    11  	"context"
    12  	"fmt"
    13  	"internal/testenv"
    14  	"internal/txtar"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"runtime"
    19  	"strings"
    20  	"testing"
    21  	"time"
    22  )
    23  
    24  // ToolReplacement records the name of a tool to replace
    25  // within a given GOROOT for script testing purposes.
    26  type ToolReplacement struct {
    27  	ToolName        string // e.g. compile, link, addr2line, etc
    28  	ReplacementPath string // path to replacement tool exe
    29  	EnvVar          string // env var setting (e.g. "FOO=BAR")
    30  }
    31  
    32  // RunToolScriptTest kicks off a set of script tests runs for
    33  // a tool of some sort (compiler, linker, etc). The expectation
    34  // is that we'll be called from the top level cmd/X dir for tool X,
    35  // and that instead of executing the install tool X we'll use the
    36  // test binary instead.
    37  func RunToolScriptTest(t *testing.T, repls []ToolReplacement, scriptsdir string, fixReadme bool) {
    38  	// Nearly all script tests involve doing builds, so don't
    39  	// bother here if we don't have "go build".
    40  	testenv.MustHaveGoBuild(t)
    41  
    42  	// Skip this path on plan9, which doesn't support symbolic
    43  	// links (we would have to copy too much).
    44  	if runtime.GOOS == "plan9" {
    45  		t.Skipf("no symlinks on plan9")
    46  	}
    47  
    48  	// Locate our Go tool.
    49  	gotool, err := testenv.GoTool()
    50  	if err != nil {
    51  		t.Fatalf("locating go tool: %v", err)
    52  	}
    53  
    54  	goEnv := func(name string) string {
    55  		out, err := exec.Command(gotool, "env", name).CombinedOutput()
    56  		if err != nil {
    57  			t.Fatalf("go env %s: %v\n%s", name, err, out)
    58  		}
    59  		return strings.TrimSpace(string(out))
    60  	}
    61  
    62  	// Construct an initial set of commands + conditions to make available
    63  	// to the script tests.
    64  	cmds := DefaultCmds()
    65  	conds := DefaultConds()
    66  
    67  	addcmd := func(name string, cmd script.Cmd) {
    68  		if _, ok := cmds[name]; ok {
    69  			panic(fmt.Sprintf("command %q is already registered", name))
    70  		}
    71  		cmds[name] = cmd
    72  	}
    73  
    74  	prependToPath := func(env []string, dir string) {
    75  		found := false
    76  		for k := range env {
    77  			ev := env[k]
    78  			if !strings.HasPrefix(ev, "PATH=") {
    79  				continue
    80  			}
    81  			oldpath := ev[5:]
    82  			env[k] = "PATH=" + dir + string(filepath.ListSeparator) + oldpath
    83  			found = true
    84  			break
    85  		}
    86  		if !found {
    87  			t.Fatalf("could not update PATH")
    88  		}
    89  	}
    90  
    91  	setenv := func(env []string, varname, val string) []string {
    92  		pref := varname + "="
    93  		found := false
    94  		for k := range env {
    95  			if !strings.HasPrefix(env[k], pref) {
    96  				continue
    97  			}
    98  			env[k] = pref + val
    99  			found = true
   100  			break
   101  		}
   102  		if !found {
   103  			env = append(env, varname+"="+val)
   104  		}
   105  		return env
   106  	}
   107  
   108  	interrupt := func(cmd *exec.Cmd) error {
   109  		return cmd.Process.Signal(os.Interrupt)
   110  	}
   111  	gracePeriod := 60 * time.Second // arbitrary
   112  
   113  	// Set up an alternate go root for running script tests, since it
   114  	// is possible that we might want to replace one of the installed
   115  	// tools with a unit test executable.
   116  	goroot := goEnv("GOROOT")
   117  	tmpdir := t.TempDir()
   118  	tgr := SetupTestGoRoot(t, tmpdir, goroot)
   119  
   120  	// Replace tools if appropriate
   121  	for _, repl := range repls {
   122  		ReplaceGoToolInTestGoRoot(t, tgr, repl.ToolName, repl.ReplacementPath)
   123  	}
   124  
   125  	// Add in commands for "go" and "cc".
   126  	testgo := filepath.Join(tgr, "bin", "go")
   127  	gocmd := script.Program(testgo, interrupt, gracePeriod)
   128  	addcmd("go", gocmd)
   129  	cmdExec := cmds["exec"]
   130  	addcmd("cc", scriptCC(cmdExec, goEnv("CC")))
   131  
   132  	// Add various helpful conditions related to builds and toolchain use.
   133  	goHostOS, goHostArch := goEnv("GOHOSTOS"), goEnv("GOHOSTARCH")
   134  	AddToolChainScriptConditions(t, conds, goHostOS, goHostArch)
   135  
   136  	// Environment setup.
   137  	env := os.Environ()
   138  	prependToPath(env, filepath.Join(tgr, "bin"))
   139  	env = setenv(env, "GOROOT", tgr)
   140  	// GOOS and GOARCH are expected to be set by the toolchain script conditions.
   141  	env = setenv(env, "GOOS", runtime.GOOS)
   142  	env = setenv(env, "GOARCH", runtime.GOARCH)
   143  	for _, repl := range repls {
   144  		// consistency check
   145  		chunks := strings.Split(repl.EnvVar, "=")
   146  		if len(chunks) != 2 {
   147  			t.Fatalf("malformed env var setting: %s", repl.EnvVar)
   148  		}
   149  		env = append(env, repl.EnvVar)
   150  	}
   151  
   152  	// Manufacture engine...
   153  	engine := &script.Engine{
   154  		Conds: conds,
   155  		Cmds:  cmds,
   156  		Quiet: !testing.Verbose(),
   157  	}
   158  
   159  	t.Run("README", func(t *testing.T) {
   160  		checkScriptReadme(t, engine, env, scriptsdir, gotool, fixReadme)
   161  	})
   162  
   163  	// ... and kick off tests.
   164  	ctx := context.Background()
   165  	pattern := filepath.Join(scriptsdir, "*.txt")
   166  	RunTests(t, ctx, engine, env, pattern)
   167  }
   168  
   169  // RunTests kicks off one or more script-based tests using the
   170  // specified engine, running all test files that match pattern.
   171  // This function adapted from Russ's rsc.io/script/scripttest#Run
   172  // function, which was in turn forked off cmd/go's runner.
   173  func RunTests(t *testing.T, ctx context.Context, engine *script.Engine, env []string, pattern string) {
   174  	gracePeriod := 100 * time.Millisecond
   175  	if deadline, ok := t.Deadline(); ok {
   176  		timeout := time.Until(deadline)
   177  
   178  		// If time allows, increase the termination grace period to 5% of the
   179  		// remaining time.
   180  		if gp := timeout / 20; gp > gracePeriod {
   181  			gracePeriod = gp
   182  		}
   183  
   184  		// When we run commands that execute subprocesses, we want to
   185  		// reserve two grace periods to clean up. We will send the
   186  		// first termination signal when the context expires, then
   187  		// wait one grace period for the process to produce whatever
   188  		// useful output it can (such as a stack trace). After the
   189  		// first grace period expires, we'll escalate to os.Kill,
   190  		// leaving the second grace period for the test function to
   191  		// record its output before the test process itself
   192  		// terminates.
   193  		timeout -= 2 * gracePeriod
   194  
   195  		var cancel context.CancelFunc
   196  		ctx, cancel = context.WithTimeout(ctx, timeout)
   197  		t.Cleanup(cancel)
   198  	}
   199  
   200  	files, _ := filepath.Glob(pattern)
   201  	if len(files) == 0 {
   202  		t.Fatal("no testdata")
   203  	}
   204  	for _, file := range files {
   205  		file := file
   206  		name := strings.TrimSuffix(filepath.Base(file), ".txt")
   207  		t.Run(name, func(t *testing.T) {
   208  			t.Parallel()
   209  
   210  			workdir := t.TempDir()
   211  			s, err := script.NewState(ctx, workdir, env)
   212  			if err != nil {
   213  				t.Fatal(err)
   214  			}
   215  
   216  			// Unpack archive.
   217  			a, err := txtar.ParseFile(file)
   218  			if err != nil {
   219  				t.Fatal(err)
   220  			}
   221  			initScriptDirs(t, s)
   222  			if err := s.ExtractFiles(a); err != nil {
   223  				t.Fatal(err)
   224  			}
   225  
   226  			t.Log(time.Now().UTC().Format(time.RFC3339))
   227  			work, _ := s.LookupEnv("WORK")
   228  			t.Logf("$WORK=%s", work)
   229  
   230  			// Note: Do not use filepath.Base(file) here:
   231  			// editors that can jump to file:line references in the output
   232  			// will work better seeing the full path relative to the
   233  			// directory containing the command being tested
   234  			// (e.g. where "go test" command is usually run).
   235  			Run(t, engine, s, file, bytes.NewReader(a.Comment))
   236  		})
   237  	}
   238  }
   239  
   240  func initScriptDirs(t testing.TB, s *script.State) {
   241  	must := func(err error) {
   242  		if err != nil {
   243  			t.Helper()
   244  			t.Fatal(err)
   245  		}
   246  	}
   247  
   248  	work := s.Getwd()
   249  	must(s.Setenv("WORK", work))
   250  	must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
   251  	must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
   252  }
   253  
   254  func tempEnvName() string {
   255  	switch runtime.GOOS {
   256  	case "windows":
   257  		return "TMP"
   258  	case "plan9":
   259  		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
   260  	default:
   261  		return "TMPDIR"
   262  	}
   263  }
   264  
   265  // scriptCC runs the platform C compiler.
   266  func scriptCC(cmdExec script.Cmd, ccexe string) script.Cmd {
   267  	return script.Command(
   268  		script.CmdUsage{
   269  			Summary: "run the platform C compiler",
   270  			Args:    "args...",
   271  		},
   272  		func(s *script.State, args ...string) (script.WaitFunc, error) {
   273  			return cmdExec.Run(s, append([]string{ccexe}, args...)...)
   274  		})
   275  }
   276  

View as plain text