Source file src/cmd/go/script_test.go

     1  // Copyright 2018 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  // Script-driven tests.
     6  // See testdata/script/README for an overview.
     7  
     8  package main_test
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"errors"
    14  	"flag"
    15  	"fmt"
    16  	"go/build"
    17  	"internal/testenv"
    18  	"io/fs"
    19  	"os"
    20  	"os/exec"
    21  	"path/filepath"
    22  	"regexp"
    23  	"runtime"
    24  	"strconv"
    25  	"strings"
    26  	"sync"
    27  	"testing"
    28  	"time"
    29  
    30  	"cmd/go/internal/cfg"
    31  	"cmd/go/internal/imports"
    32  	"cmd/go/internal/par"
    33  	"cmd/go/internal/robustio"
    34  	"cmd/go/internal/work"
    35  	"cmd/internal/sys"
    36  
    37  	"golang.org/x/tools/txtar"
    38  )
    39  
    40  var testSum = flag.String("testsum", "", `may be tidy, listm, or listall. If set, TestScript generates a go.sum file at the beginning of each test and updates test files if they pass.`)
    41  
    42  // TestScript runs the tests in testdata/script/*.txt.
    43  func TestScript(t *testing.T) {
    44  	testenv.MustHaveGoBuild(t)
    45  	testenv.SkipIfShortAndSlow(t)
    46  
    47  	var (
    48  		ctx         = context.Background()
    49  		gracePeriod = 100 * time.Millisecond
    50  	)
    51  	if deadline, ok := t.Deadline(); ok {
    52  		timeout := time.Until(deadline)
    53  
    54  		// If time allows, increase the termination grace period to 5% of the
    55  		// remaining time.
    56  		if gp := timeout / 20; gp > gracePeriod {
    57  			gracePeriod = gp
    58  		}
    59  
    60  		// When we run commands that execute subprocesses, we want to reserve two
    61  		// grace periods to clean up. We will send the first termination signal when
    62  		// the context expires, then wait one grace period for the process to
    63  		// produce whatever useful output it can (such as a stack trace). After the
    64  		// first grace period expires, we'll escalate to os.Kill, leaving the second
    65  		// grace period for the test function to record its output before the test
    66  		// process itself terminates.
    67  		timeout -= 2 * gracePeriod
    68  
    69  		var cancel context.CancelFunc
    70  		ctx, cancel = context.WithTimeout(ctx, timeout)
    71  		t.Cleanup(cancel)
    72  	}
    73  
    74  	files, err := filepath.Glob("testdata/script/*.txt")
    75  	if err != nil {
    76  		t.Fatal(err)
    77  	}
    78  	for _, file := range files {
    79  		file := file
    80  		name := strings.TrimSuffix(filepath.Base(file), ".txt")
    81  		t.Run(name, func(t *testing.T) {
    82  			t.Parallel()
    83  			ctx, cancel := context.WithCancel(ctx)
    84  			ts := &testScript{
    85  				t:           t,
    86  				ctx:         ctx,
    87  				cancel:      cancel,
    88  				gracePeriod: gracePeriod,
    89  				name:        name,
    90  				file:        file,
    91  			}
    92  			ts.setup()
    93  			if !*testWork {
    94  				defer removeAll(ts.workdir)
    95  			}
    96  			ts.run()
    97  			cancel()
    98  		})
    99  	}
   100  }
   101  
   102  // A testScript holds execution state for a single test script.
   103  type testScript struct {
   104  	t           *testing.T
   105  	ctx         context.Context
   106  	cancel      context.CancelFunc
   107  	gracePeriod time.Duration
   108  	workdir     string            // temporary work dir ($WORK)
   109  	log         bytes.Buffer      // test execution log (printed at end of test)
   110  	mark        int               // offset of next log truncation
   111  	cd          string            // current directory during test execution; initially $WORK/gopath/src
   112  	name        string            // short name of test ("foo")
   113  	file        string            // full file name ("testdata/script/foo.txt")
   114  	lineno      int               // line number currently executing
   115  	line        string            // line currently executing
   116  	env         []string          // environment list (for os/exec)
   117  	envMap      map[string]string // environment mapping (matches env)
   118  	stdout      string            // standard output from last 'go' command; for 'stdout' command
   119  	stderr      string            // standard error from last 'go' command; for 'stderr' command
   120  	stopped     bool              // test wants to stop early
   121  	start       time.Time         // time phase started
   122  	background  []*backgroundCmd  // backgrounded 'exec' and 'go' commands
   123  }
   124  
   125  type backgroundCmd struct {
   126  	want           simpleStatus
   127  	args           []string
   128  	done           <-chan struct{}
   129  	err            error
   130  	stdout, stderr strings.Builder
   131  }
   132  
   133  type simpleStatus string
   134  
   135  const (
   136  	success          simpleStatus = ""
   137  	failure          simpleStatus = "!"
   138  	successOrFailure simpleStatus = "?"
   139  )
   140  
   141  var extraEnvKeys = []string{
   142  	"SYSTEMROOT",         // must be preserved on Windows to find DLLs; golang.org/issue/25210
   143  	"WINDIR",             // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711
   144  	"LD_LIBRARY_PATH",    // must be preserved on Unix systems to find shared libraries
   145  	"LIBRARY_PATH",       // allow override of non-standard static library paths
   146  	"C_INCLUDE_PATH",     // allow override non-standard include paths
   147  	"CC",                 // don't lose user settings when invoking cgo
   148  	"GO_TESTING_GOTOOLS", // for gccgo testing
   149  	"GCCGO",              // for gccgo testing
   150  	"GCCGOTOOLDIR",       // for gccgo testing
   151  }
   152  
   153  // setup sets up the test execution temporary directory and environment.
   154  func (ts *testScript) setup() {
   155  	if err := ts.ctx.Err(); err != nil {
   156  		ts.t.Fatalf("test interrupted during setup: %v", err)
   157  	}
   158  
   159  	StartProxy()
   160  	ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name)
   161  	ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777))
   162  	ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777))
   163  	ts.cd = filepath.Join(ts.workdir, "gopath/src")
   164  	ts.env = []string{
   165  		"WORK=" + ts.workdir, // must be first for ts.abbrev
   166  		"PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"),
   167  		homeEnvName() + "=/no-home",
   168  		"CCACHE_DISABLE=1", // ccache breaks with non-existent HOME
   169  		"GOARCH=" + runtime.GOARCH,
   170  		"GOCACHE=" + testGOCACHE,
   171  		"GODEBUG=" + os.Getenv("GODEBUG"),
   172  		"GOEXE=" + cfg.ExeSuffix,
   173  		"GOOS=" + runtime.GOOS,
   174  		"GOPATH=" + filepath.Join(ts.workdir, "gopath"),
   175  		"GOPROXY=" + proxyURL,
   176  		"GOPRIVATE=",
   177  		"GOROOT=" + testGOROOT,
   178  		"GOROOT_FINAL=" + os.Getenv("GOROOT_FINAL"), // causes spurious rebuilds and breaks the "stale" built-in if not propagated
   179  		"GOTRACEBACK=system",
   180  		"TESTGO_GOROOT=" + testGOROOT,
   181  		"GOSUMDB=" + testSumDBVerifierKey,
   182  		"GONOPROXY=",
   183  		"GONOSUMDB=",
   184  		"GOVCS=*:all",
   185  		"PWD=" + ts.cd,
   186  		tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"),
   187  		"devnull=" + os.DevNull,
   188  		"goversion=" + goVersion(ts),
   189  		":=" + string(os.PathListSeparator),
   190  		"/=" + string(os.PathSeparator),
   191  	}
   192  	if !testenv.HasExternalNetwork() {
   193  		ts.env = append(ts.env, "TESTGONETWORK=panic", "TESTGOVCS=panic")
   194  	}
   195  
   196  	if runtime.GOOS == "plan9" {
   197  		ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path"))
   198  	}
   199  
   200  	for _, key := range extraEnvKeys {
   201  		if val := os.Getenv(key); val != "" {
   202  			ts.env = append(ts.env, key+"="+val)
   203  		}
   204  	}
   205  
   206  	ts.envMap = make(map[string]string)
   207  	for _, kv := range ts.env {
   208  		if i := strings.Index(kv, "="); i >= 0 {
   209  			ts.envMap[kv[:i]] = kv[i+1:]
   210  		}
   211  	}
   212  }
   213  
   214  // goVersion returns the current Go version.
   215  func goVersion(ts *testScript) string {
   216  	tags := build.Default.ReleaseTags
   217  	version := tags[len(tags)-1]
   218  	if !regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`).MatchString(version) {
   219  		ts.fatalf("invalid go version %q", version)
   220  	}
   221  	return version[2:]
   222  }
   223  
   224  var execCache par.Cache
   225  
   226  // run runs the test script.
   227  func (ts *testScript) run() {
   228  	// Truncate log at end of last phase marker,
   229  	// discarding details of successful phase.
   230  	rewind := func() {
   231  		if !testing.Verbose() {
   232  			ts.log.Truncate(ts.mark)
   233  		}
   234  	}
   235  
   236  	// Insert elapsed time for phase at end of phase marker
   237  	markTime := func() {
   238  		if ts.mark > 0 && !ts.start.IsZero() {
   239  			afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
   240  			ts.log.Truncate(ts.mark - 1) // cut \n and afterMark
   241  			fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds())
   242  			ts.log.Write(afterMark)
   243  		}
   244  		ts.start = time.Time{}
   245  	}
   246  
   247  	defer func() {
   248  		// On a normal exit from the test loop, background processes are cleaned up
   249  		// before we print PASS. If we return early (e.g., due to a test failure),
   250  		// don't print anything about the processes that were still running.
   251  		ts.cancel()
   252  		for _, bg := range ts.background {
   253  			<-bg.done
   254  		}
   255  		ts.background = nil
   256  
   257  		markTime()
   258  		// Flush testScript log to testing.T log.
   259  		ts.t.Log("\n" + ts.abbrev(ts.log.String()))
   260  	}()
   261  
   262  	// Unpack archive.
   263  	a, err := txtar.ParseFile(ts.file)
   264  	ts.check(err)
   265  	for _, f := range a.Files {
   266  		name := ts.mkabs(ts.expand(f.Name, false))
   267  		ts.check(os.MkdirAll(filepath.Dir(name), 0777))
   268  		ts.check(os.WriteFile(name, f.Data, 0666))
   269  	}
   270  
   271  	// With -v or -testwork, start log with full environment.
   272  	if *testWork || testing.Verbose() {
   273  		// Display environment.
   274  		ts.cmdEnv(success, nil)
   275  		fmt.Fprintf(&ts.log, "\n")
   276  		ts.mark = ts.log.Len()
   277  	}
   278  
   279  	// With -testsum, if a go.mod file is present in the test's initial
   280  	// working directory, run 'go mod tidy'.
   281  	if *testSum != "" {
   282  		if ts.updateSum(a) {
   283  			defer func() {
   284  				if ts.t.Failed() {
   285  					return
   286  				}
   287  				data := txtar.Format(a)
   288  				if err := os.WriteFile(ts.file, data, 0666); err != nil {
   289  					ts.t.Errorf("rewriting test file: %v", err)
   290  				}
   291  			}()
   292  		}
   293  	}
   294  
   295  	// Run script.
   296  	// See testdata/script/README for documentation of script form.
   297  	script := string(a.Comment)
   298  Script:
   299  	for script != "" {
   300  		// Extract next line.
   301  		ts.lineno++
   302  		var line string
   303  		if i := strings.Index(script, "\n"); i >= 0 {
   304  			line, script = script[:i], script[i+1:]
   305  		} else {
   306  			line, script = script, ""
   307  		}
   308  
   309  		// # is a comment indicating the start of new phase.
   310  		if strings.HasPrefix(line, "#") {
   311  			// If there was a previous phase, it succeeded,
   312  			// so rewind the log to delete its details (unless -v is in use).
   313  			// If nothing has happened at all since the mark,
   314  			// rewinding is a no-op and adding elapsed time
   315  			// for doing nothing is meaningless, so don't.
   316  			if ts.log.Len() > ts.mark {
   317  				rewind()
   318  				markTime()
   319  			}
   320  			// Print phase heading and mark start of phase output.
   321  			fmt.Fprintf(&ts.log, "%s\n", line)
   322  			ts.mark = ts.log.Len()
   323  			ts.start = time.Now()
   324  			continue
   325  		}
   326  
   327  		// Parse input line. Ignore blanks entirely.
   328  		parsed := ts.parse(line)
   329  		if parsed.name == "" {
   330  			if parsed.want != "" || len(parsed.conds) > 0 {
   331  				ts.fatalf("missing command")
   332  			}
   333  			continue
   334  		}
   335  
   336  		// Echo command to log.
   337  		fmt.Fprintf(&ts.log, "> %s\n", line)
   338  
   339  		for _, cond := range parsed.conds {
   340  			if err := ts.ctx.Err(); err != nil {
   341  				ts.fatalf("test interrupted: %v", err)
   342  			}
   343  
   344  			// Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short).
   345  			//
   346  			// NOTE: If you make changes here, update testdata/script/README too!
   347  			//
   348  			ok := false
   349  			switch cond.tag {
   350  			case runtime.GOOS, runtime.GOARCH, runtime.Compiler:
   351  				ok = true
   352  			case "short":
   353  				ok = testing.Short()
   354  			case "cgo":
   355  				ok = canCgo
   356  			case "msan":
   357  				ok = canMSan
   358  			case "asan":
   359  				ok = canASan
   360  			case "race":
   361  				ok = canRace
   362  			case "fuzz":
   363  				ok = canFuzz
   364  			case "fuzz-instrumented":
   365  				ok = fuzzInstrumented
   366  			case "net":
   367  				ok = testenv.HasExternalNetwork()
   368  			case "link":
   369  				ok = testenv.HasLink()
   370  			case "root":
   371  				ok = os.Geteuid() == 0
   372  			case "symlink":
   373  				ok = testenv.HasSymlink()
   374  			case "case-sensitive":
   375  				ok = isCaseSensitive(ts.t)
   376  			default:
   377  				if strings.HasPrefix(cond.tag, "exec:") {
   378  					prog := cond.tag[len("exec:"):]
   379  					ok = execCache.Do(prog, func() any {
   380  						if runtime.GOOS == "plan9" && prog == "git" {
   381  							// The Git command is usually not the real Git on Plan 9.
   382  							// See https://golang.org/issues/29640.
   383  							return false
   384  						}
   385  						_, err := exec.LookPath(prog)
   386  						return err == nil
   387  					}).(bool)
   388  					break
   389  				}
   390  				if strings.HasPrefix(cond.tag, "GODEBUG:") {
   391  					value := strings.TrimPrefix(cond.tag, "GODEBUG:")
   392  					parts := strings.Split(os.Getenv("GODEBUG"), ",")
   393  					for _, p := range parts {
   394  						if strings.TrimSpace(p) == value {
   395  							ok = true
   396  							break
   397  						}
   398  					}
   399  					break
   400  				}
   401  				if strings.HasPrefix(cond.tag, "buildmode:") {
   402  					value := strings.TrimPrefix(cond.tag, "buildmode:")
   403  					ok = sys.BuildModeSupported(runtime.Compiler, value, runtime.GOOS, runtime.GOARCH)
   404  					break
   405  				}
   406  				if !imports.KnownArch[cond.tag] && !imports.KnownOS[cond.tag] && cond.tag != "gc" && cond.tag != "gccgo" {
   407  					ts.fatalf("unknown condition %q", cond.tag)
   408  				}
   409  			}
   410  			if ok != cond.want {
   411  				// Don't run rest of line.
   412  				continue Script
   413  			}
   414  		}
   415  
   416  		// Run command.
   417  		cmd := scriptCmds[parsed.name]
   418  		if cmd == nil {
   419  			ts.fatalf("unknown command %q", parsed.name)
   420  		}
   421  		cmd(ts, parsed.want, parsed.args)
   422  
   423  		// Command can ask script to stop early.
   424  		if ts.stopped {
   425  			// Break instead of returning, so that we check the status of any
   426  			// background processes and print PASS.
   427  			break
   428  		}
   429  	}
   430  
   431  	ts.cancel()
   432  	ts.cmdWait(success, nil)
   433  
   434  	// Final phase ended.
   435  	rewind()
   436  	markTime()
   437  	if !ts.stopped {
   438  		fmt.Fprintf(&ts.log, "PASS\n")
   439  	}
   440  }
   441  
   442  var (
   443  	onceCaseSensitive sync.Once
   444  	caseSensitive     bool
   445  )
   446  
   447  func isCaseSensitive(t *testing.T) bool {
   448  	onceCaseSensitive.Do(func() {
   449  		tmpdir, err := os.MkdirTemp("", "case-sensitive")
   450  		if err != nil {
   451  			t.Fatal("failed to create directory to determine case-sensitivity:", err)
   452  		}
   453  		defer os.RemoveAll(tmpdir)
   454  
   455  		fcap := filepath.Join(tmpdir, "FILE")
   456  		if err := os.WriteFile(fcap, []byte{}, 0644); err != nil {
   457  			t.Fatal("error writing file to determine case-sensitivity:", err)
   458  		}
   459  
   460  		flow := filepath.Join(tmpdir, "file")
   461  		_, err = os.ReadFile(flow)
   462  		switch {
   463  		case err == nil:
   464  			caseSensitive = false
   465  			return
   466  		case os.IsNotExist(err):
   467  			caseSensitive = true
   468  			return
   469  		default:
   470  			t.Fatal("unexpected error reading file when determining case-sensitivity:", err)
   471  		}
   472  	})
   473  
   474  	return caseSensitive
   475  }
   476  
   477  // scriptCmds are the script command implementations.
   478  // Keep list and the implementations below sorted by name.
   479  //
   480  // NOTE: If you make changes here, update testdata/script/README too!
   481  //
   482  var scriptCmds = map[string]func(*testScript, simpleStatus, []string){
   483  	"addcrlf": (*testScript).cmdAddcrlf,
   484  	"cc":      (*testScript).cmdCc,
   485  	"cd":      (*testScript).cmdCd,
   486  	"chmod":   (*testScript).cmdChmod,
   487  	"cmp":     (*testScript).cmdCmp,
   488  	"cmpenv":  (*testScript).cmdCmpenv,
   489  	"cp":      (*testScript).cmdCp,
   490  	"env":     (*testScript).cmdEnv,
   491  	"exec":    (*testScript).cmdExec,
   492  	"exists":  (*testScript).cmdExists,
   493  	"go":      (*testScript).cmdGo,
   494  	"grep":    (*testScript).cmdGrep,
   495  	"mkdir":   (*testScript).cmdMkdir,
   496  	"mv":      (*testScript).cmdMv,
   497  	"rm":      (*testScript).cmdRm,
   498  	"skip":    (*testScript).cmdSkip,
   499  	"stale":   (*testScript).cmdStale,
   500  	"stderr":  (*testScript).cmdStderr,
   501  	"stdout":  (*testScript).cmdStdout,
   502  	"stop":    (*testScript).cmdStop,
   503  	"symlink": (*testScript).cmdSymlink,
   504  	"wait":    (*testScript).cmdWait,
   505  }
   506  
   507  // When expanding shell variables for these commands, we apply regexp quoting to
   508  // expanded strings within the first argument.
   509  var regexpCmd = map[string]bool{
   510  	"grep":   true,
   511  	"stderr": true,
   512  	"stdout": true,
   513  }
   514  
   515  // addcrlf adds CRLF line endings to the named files.
   516  func (ts *testScript) cmdAddcrlf(want simpleStatus, args []string) {
   517  	if len(args) == 0 {
   518  		ts.fatalf("usage: addcrlf file...")
   519  	}
   520  
   521  	for _, file := range args {
   522  		file = ts.mkabs(file)
   523  		data, err := os.ReadFile(file)
   524  		ts.check(err)
   525  		ts.check(os.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666))
   526  	}
   527  }
   528  
   529  // cc runs the C compiler along with platform specific options.
   530  func (ts *testScript) cmdCc(want simpleStatus, args []string) {
   531  	if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
   532  		ts.fatalf("usage: cc args... [&]")
   533  	}
   534  
   535  	var b work.Builder
   536  	b.Init()
   537  	ts.cmdExec(want, append(b.GccCmd(".", ""), args...))
   538  	robustio.RemoveAll(b.WorkDir)
   539  }
   540  
   541  // cd changes to a different directory.
   542  func (ts *testScript) cmdCd(want simpleStatus, args []string) {
   543  	if want != success {
   544  		ts.fatalf("unsupported: %v cd", want)
   545  	}
   546  	if len(args) != 1 {
   547  		ts.fatalf("usage: cd dir")
   548  	}
   549  
   550  	dir := filepath.FromSlash(args[0])
   551  	if !filepath.IsAbs(dir) {
   552  		dir = filepath.Join(ts.cd, dir)
   553  	}
   554  	info, err := os.Stat(dir)
   555  	if os.IsNotExist(err) {
   556  		ts.fatalf("directory %s does not exist", dir)
   557  	}
   558  	ts.check(err)
   559  	if !info.IsDir() {
   560  		ts.fatalf("%s is not a directory", dir)
   561  	}
   562  	ts.cd = dir
   563  	ts.envMap["PWD"] = dir
   564  	fmt.Fprintf(&ts.log, "%s\n", ts.cd)
   565  }
   566  
   567  // chmod changes permissions for a file or directory.
   568  func (ts *testScript) cmdChmod(want simpleStatus, args []string) {
   569  	if want != success {
   570  		ts.fatalf("unsupported: %v chmod", want)
   571  	}
   572  	if len(args) < 2 {
   573  		ts.fatalf("usage: chmod perm paths...")
   574  	}
   575  	perm, err := strconv.ParseUint(args[0], 0, 32)
   576  	if err != nil || perm&uint64(fs.ModePerm) != perm {
   577  		ts.fatalf("invalid mode: %s", args[0])
   578  	}
   579  	for _, arg := range args[1:] {
   580  		path := arg
   581  		if !filepath.IsAbs(path) {
   582  			path = filepath.Join(ts.cd, arg)
   583  		}
   584  		err := os.Chmod(path, fs.FileMode(perm))
   585  		ts.check(err)
   586  	}
   587  }
   588  
   589  // cmp compares two files.
   590  func (ts *testScript) cmdCmp(want simpleStatus, args []string) {
   591  	quiet := false
   592  	if len(args) > 0 && args[0] == "-q" {
   593  		quiet = true
   594  		args = args[1:]
   595  	}
   596  	if len(args) != 2 {
   597  		ts.fatalf("usage: cmp file1 file2")
   598  	}
   599  	ts.doCmdCmp(want, args, false, quiet)
   600  }
   601  
   602  // cmpenv compares two files with environment variable substitution.
   603  func (ts *testScript) cmdCmpenv(want simpleStatus, args []string) {
   604  	quiet := false
   605  	if len(args) > 0 && args[0] == "-q" {
   606  		quiet = true
   607  		args = args[1:]
   608  	}
   609  	if len(args) != 2 {
   610  		ts.fatalf("usage: cmpenv file1 file2")
   611  	}
   612  	ts.doCmdCmp(want, args, true, quiet)
   613  }
   614  
   615  func (ts *testScript) doCmdCmp(want simpleStatus, args []string, env, quiet bool) {
   616  	name1, name2 := args[0], args[1]
   617  	var text1, text2 string
   618  	switch name1 {
   619  	case "stdout":
   620  		text1 = ts.stdout
   621  	case "stderr":
   622  		text1 = ts.stderr
   623  	default:
   624  		data, err := os.ReadFile(ts.mkabs(name1))
   625  		ts.check(err)
   626  		text1 = string(data)
   627  	}
   628  
   629  	data, err := os.ReadFile(ts.mkabs(name2))
   630  	ts.check(err)
   631  	text2 = string(data)
   632  
   633  	if env {
   634  		text1 = ts.expand(text1, false)
   635  		text2 = ts.expand(text2, false)
   636  	}
   637  
   638  	eq := text1 == text2
   639  	if !eq && !quiet && want != failure {
   640  		fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2))
   641  	}
   642  	switch want {
   643  	case failure:
   644  		if eq {
   645  			ts.fatalf("%s and %s do not differ", name1, name2)
   646  		}
   647  	case success:
   648  		if !eq {
   649  			ts.fatalf("%s and %s differ", name1, name2)
   650  		}
   651  	case successOrFailure:
   652  		if eq {
   653  			fmt.Fprintf(&ts.log, "%s and %s do not differ\n", name1, name2)
   654  		} else {
   655  			fmt.Fprintf(&ts.log, "%s and %s differ\n", name1, name2)
   656  		}
   657  	default:
   658  		ts.fatalf("unsupported: %v cmp", want)
   659  	}
   660  }
   661  
   662  // cp copies files, maybe eventually directories.
   663  func (ts *testScript) cmdCp(want simpleStatus, args []string) {
   664  	if len(args) < 2 {
   665  		ts.fatalf("usage: cp src... dst")
   666  	}
   667  
   668  	dst := ts.mkabs(args[len(args)-1])
   669  	info, err := os.Stat(dst)
   670  	dstDir := err == nil && info.IsDir()
   671  	if len(args) > 2 && !dstDir {
   672  		ts.fatalf("cp: destination %s is not a directory", dst)
   673  	}
   674  
   675  	for _, arg := range args[:len(args)-1] {
   676  		var (
   677  			src  string
   678  			data []byte
   679  			mode fs.FileMode
   680  		)
   681  		switch arg {
   682  		case "stdout":
   683  			src = arg
   684  			data = []byte(ts.stdout)
   685  			mode = 0666
   686  		case "stderr":
   687  			src = arg
   688  			data = []byte(ts.stderr)
   689  			mode = 0666
   690  		default:
   691  			src = ts.mkabs(arg)
   692  			info, err := os.Stat(src)
   693  			ts.check(err)
   694  			mode = info.Mode() & 0777
   695  			data, err = os.ReadFile(src)
   696  			ts.check(err)
   697  		}
   698  		targ := dst
   699  		if dstDir {
   700  			targ = filepath.Join(dst, filepath.Base(src))
   701  		}
   702  		err := os.WriteFile(targ, data, mode)
   703  		switch want {
   704  		case failure:
   705  			if err == nil {
   706  				ts.fatalf("unexpected command success")
   707  			}
   708  		case success:
   709  			ts.check(err)
   710  		}
   711  	}
   712  }
   713  
   714  // env displays or adds to the environment.
   715  func (ts *testScript) cmdEnv(want simpleStatus, args []string) {
   716  	if want != success {
   717  		ts.fatalf("unsupported: %v env", want)
   718  	}
   719  
   720  	conv := func(s string) string { return s }
   721  	if len(args) > 0 && args[0] == "-r" {
   722  		conv = regexp.QuoteMeta
   723  		args = args[1:]
   724  	}
   725  
   726  	var out strings.Builder
   727  	if len(args) == 0 {
   728  		printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once
   729  		for _, kv := range ts.env {
   730  			k := kv[:strings.Index(kv, "=")]
   731  			if !printed[k] {
   732  				fmt.Fprintf(&out, "%s=%s\n", k, ts.envMap[k])
   733  			}
   734  		}
   735  	} else {
   736  		for _, env := range args {
   737  			i := strings.Index(env, "=")
   738  			if i < 0 {
   739  				// Display value instead of setting it.
   740  				fmt.Fprintf(&out, "%s=%s\n", env, ts.envMap[env])
   741  				continue
   742  			}
   743  			key, val := env[:i], conv(env[i+1:])
   744  			ts.env = append(ts.env, key+"="+val)
   745  			ts.envMap[key] = val
   746  		}
   747  	}
   748  	if out.Len() > 0 || len(args) > 0 {
   749  		ts.stdout = out.String()
   750  		ts.log.WriteString(out.String())
   751  	}
   752  }
   753  
   754  // exec runs the given command.
   755  func (ts *testScript) cmdExec(want simpleStatus, args []string) {
   756  	if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
   757  		ts.fatalf("usage: exec program [args...] [&]")
   758  	}
   759  
   760  	background := false
   761  	if len(args) > 0 && args[len(args)-1] == "&" {
   762  		background = true
   763  		args = args[:len(args)-1]
   764  	}
   765  
   766  	bg, err := ts.startBackground(want, args[0], args[1:]...)
   767  	if err != nil {
   768  		ts.fatalf("unexpected error starting command: %v", err)
   769  	}
   770  	if background {
   771  		ts.stdout, ts.stderr = "", ""
   772  		ts.background = append(ts.background, bg)
   773  		return
   774  	}
   775  
   776  	<-bg.done
   777  	ts.stdout = bg.stdout.String()
   778  	ts.stderr = bg.stderr.String()
   779  	if ts.stdout != "" {
   780  		fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
   781  	}
   782  	if ts.stderr != "" {
   783  		fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
   784  	}
   785  	if bg.err != nil {
   786  		fmt.Fprintf(&ts.log, "[%v]\n", bg.err)
   787  	}
   788  	ts.checkCmd(bg)
   789  }
   790  
   791  // exists checks that the list of files exists.
   792  func (ts *testScript) cmdExists(want simpleStatus, args []string) {
   793  	if want == successOrFailure {
   794  		ts.fatalf("unsupported: %v exists", want)
   795  	}
   796  	var readonly, exec bool
   797  loop:
   798  	for len(args) > 0 {
   799  		switch args[0] {
   800  		case "-readonly":
   801  			readonly = true
   802  			args = args[1:]
   803  		case "-exec":
   804  			exec = true
   805  			args = args[1:]
   806  		default:
   807  			break loop
   808  		}
   809  	}
   810  	if len(args) == 0 {
   811  		ts.fatalf("usage: exists [-readonly] [-exec] file...")
   812  	}
   813  
   814  	for _, file := range args {
   815  		file = ts.mkabs(file)
   816  		info, err := os.Stat(file)
   817  		if err == nil && want == failure {
   818  			what := "file"
   819  			if info.IsDir() {
   820  				what = "directory"
   821  			}
   822  			ts.fatalf("%s %s unexpectedly exists", what, file)
   823  		}
   824  		if err != nil && want == success {
   825  			ts.fatalf("%s does not exist", file)
   826  		}
   827  		if err == nil && want == success && readonly && info.Mode()&0222 != 0 {
   828  			ts.fatalf("%s exists but is writable", file)
   829  		}
   830  		if err == nil && want == success && exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
   831  			ts.fatalf("%s exists but is not executable", file)
   832  		}
   833  	}
   834  }
   835  
   836  // go runs the go command.
   837  func (ts *testScript) cmdGo(want simpleStatus, args []string) {
   838  	ts.cmdExec(want, append([]string{testGo}, args...))
   839  }
   840  
   841  // mkdir creates directories.
   842  func (ts *testScript) cmdMkdir(want simpleStatus, args []string) {
   843  	if want != success {
   844  		ts.fatalf("unsupported: %v mkdir", want)
   845  	}
   846  	if len(args) < 1 {
   847  		ts.fatalf("usage: mkdir dir...")
   848  	}
   849  	for _, arg := range args {
   850  		ts.check(os.MkdirAll(ts.mkabs(arg), 0777))
   851  	}
   852  }
   853  
   854  func (ts *testScript) cmdMv(want simpleStatus, args []string) {
   855  	if want != success {
   856  		ts.fatalf("unsupported: %v mv", want)
   857  	}
   858  	if len(args) != 2 {
   859  		ts.fatalf("usage: mv old new")
   860  	}
   861  	ts.check(os.Rename(ts.mkabs(args[0]), ts.mkabs(args[1])))
   862  }
   863  
   864  // rm removes files or directories.
   865  func (ts *testScript) cmdRm(want simpleStatus, args []string) {
   866  	if want != success {
   867  		ts.fatalf("unsupported: %v rm", want)
   868  	}
   869  	if len(args) < 1 {
   870  		ts.fatalf("usage: rm file...")
   871  	}
   872  	for _, arg := range args {
   873  		file := ts.mkabs(arg)
   874  		removeAll(file)                    // does chmod and then attempts rm
   875  		ts.check(robustio.RemoveAll(file)) // report error
   876  	}
   877  }
   878  
   879  // skip marks the test skipped.
   880  func (ts *testScript) cmdSkip(want simpleStatus, args []string) {
   881  	if len(args) > 1 {
   882  		ts.fatalf("usage: skip [msg]")
   883  	}
   884  	if want != success {
   885  		ts.fatalf("unsupported: %v skip", want)
   886  	}
   887  
   888  	// Before we mark the test as skipped, shut down any background processes and
   889  	// make sure they have returned the correct status.
   890  	ts.cancel()
   891  	ts.cmdWait(success, nil)
   892  
   893  	if len(args) == 1 {
   894  		ts.t.Skip(args[0])
   895  	}
   896  	ts.t.Skip()
   897  }
   898  
   899  // stale checks that the named build targets are stale.
   900  func (ts *testScript) cmdStale(want simpleStatus, args []string) {
   901  	if len(args) == 0 {
   902  		ts.fatalf("usage: stale target...")
   903  	}
   904  	tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{{else}}"
   905  	switch want {
   906  	case failure:
   907  		tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale: {{.StaleReason}}{{end}}"
   908  	case success:
   909  		tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}"
   910  	default:
   911  		ts.fatalf("unsupported: %v stale", want)
   912  	}
   913  	tmpl += "{{end}}"
   914  	goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...)
   915  	stdout, stderr, err := ts.exec(testGo, goArgs...)
   916  	if err != nil {
   917  		ts.fatalf("go list: %v\n%s%s", err, stdout, stderr)
   918  	}
   919  	if stdout != "" {
   920  		ts.fatalf("%s", stdout)
   921  	}
   922  }
   923  
   924  // stdout checks that the last go command standard output matches a regexp.
   925  func (ts *testScript) cmdStdout(want simpleStatus, args []string) {
   926  	scriptMatch(ts, want, args, ts.stdout, "stdout")
   927  }
   928  
   929  // stderr checks that the last go command standard output matches a regexp.
   930  func (ts *testScript) cmdStderr(want simpleStatus, args []string) {
   931  	scriptMatch(ts, want, args, ts.stderr, "stderr")
   932  }
   933  
   934  // grep checks that file content matches a regexp.
   935  // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
   936  func (ts *testScript) cmdGrep(want simpleStatus, args []string) {
   937  	scriptMatch(ts, want, args, "", "grep")
   938  }
   939  
   940  // scriptMatch implements both stdout and stderr.
   941  func scriptMatch(ts *testScript, want simpleStatus, args []string, text, name string) {
   942  	if want == successOrFailure {
   943  		ts.fatalf("unsupported: %v %s", want, name)
   944  	}
   945  
   946  	n := 0
   947  	if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
   948  		if want == failure {
   949  			ts.fatalf("cannot use -count= with negated match")
   950  		}
   951  		var err error
   952  		n, err = strconv.Atoi(args[0][len("-count="):])
   953  		if err != nil {
   954  			ts.fatalf("bad -count=: %v", err)
   955  		}
   956  		if n < 1 {
   957  			ts.fatalf("bad -count=: must be at least 1")
   958  		}
   959  		args = args[1:]
   960  	}
   961  	quiet := false
   962  	if len(args) >= 1 && args[0] == "-q" {
   963  		quiet = true
   964  		args = args[1:]
   965  	}
   966  
   967  	extraUsage := ""
   968  	wantArgs := 1
   969  	if name == "grep" {
   970  		extraUsage = " file"
   971  		wantArgs = 2
   972  	}
   973  	if len(args) != wantArgs {
   974  		ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
   975  	}
   976  
   977  	pattern := `(?m)` + args[0]
   978  	re, err := regexp.Compile(pattern)
   979  	if err != nil {
   980  		ts.fatalf("regexp.Compile(%q): %v", pattern, err)
   981  	}
   982  
   983  	isGrep := name == "grep"
   984  	if isGrep {
   985  		name = args[1] // for error messages
   986  		data, err := os.ReadFile(ts.mkabs(args[1]))
   987  		ts.check(err)
   988  		text = string(data)
   989  	}
   990  
   991  	// Matching against workdir would be misleading.
   992  	text = strings.ReplaceAll(text, ts.workdir, "$WORK")
   993  
   994  	switch want {
   995  	case failure:
   996  		if re.MatchString(text) {
   997  			if isGrep && !quiet {
   998  				fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
   999  			}
  1000  			ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
  1001  		}
  1002  
  1003  	case success:
  1004  		if !re.MatchString(text) {
  1005  			if isGrep && !quiet {
  1006  				fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
  1007  			}
  1008  			ts.fatalf("no match for %#q found in %s", pattern, name)
  1009  		}
  1010  		if n > 0 {
  1011  			count := len(re.FindAllString(text, -1))
  1012  			if count != n {
  1013  				if isGrep && !quiet {
  1014  					fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
  1015  				}
  1016  				ts.fatalf("have %d matches for %#q, want %d", count, pattern, n)
  1017  			}
  1018  		}
  1019  	}
  1020  }
  1021  
  1022  // stop stops execution of the test (marking it passed).
  1023  func (ts *testScript) cmdStop(want simpleStatus, args []string) {
  1024  	if want != success {
  1025  		ts.fatalf("unsupported: %v stop", want)
  1026  	}
  1027  	if len(args) > 1 {
  1028  		ts.fatalf("usage: stop [msg]")
  1029  	}
  1030  	if len(args) == 1 {
  1031  		fmt.Fprintf(&ts.log, "stop: %s\n", args[0])
  1032  	} else {
  1033  		fmt.Fprintf(&ts.log, "stop\n")
  1034  	}
  1035  	ts.stopped = true
  1036  }
  1037  
  1038  // symlink creates a symbolic link.
  1039  func (ts *testScript) cmdSymlink(want simpleStatus, args []string) {
  1040  	if want != success {
  1041  		ts.fatalf("unsupported: %v symlink", want)
  1042  	}
  1043  	if len(args) != 3 || args[1] != "->" {
  1044  		ts.fatalf("usage: symlink file -> target")
  1045  	}
  1046  	// Note that the link target args[2] is not interpreted with mkabs:
  1047  	// it will be interpreted relative to the directory file is in.
  1048  	ts.check(os.Symlink(args[2], ts.mkabs(args[0])))
  1049  }
  1050  
  1051  // wait waits for background commands to exit, setting stderr and stdout to their result.
  1052  func (ts *testScript) cmdWait(want simpleStatus, args []string) {
  1053  	if want != success {
  1054  		ts.fatalf("unsupported: %v wait", want)
  1055  	}
  1056  	if len(args) > 0 {
  1057  		ts.fatalf("usage: wait")
  1058  	}
  1059  
  1060  	var stdouts, stderrs []string
  1061  	for _, bg := range ts.background {
  1062  		<-bg.done
  1063  
  1064  		args := append([]string{filepath.Base(bg.args[0])}, bg.args[1:]...)
  1065  		fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.err)
  1066  
  1067  		cmdStdout := bg.stdout.String()
  1068  		if cmdStdout != "" {
  1069  			fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
  1070  			stdouts = append(stdouts, cmdStdout)
  1071  		}
  1072  
  1073  		cmdStderr := bg.stderr.String()
  1074  		if cmdStderr != "" {
  1075  			fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
  1076  			stderrs = append(stderrs, cmdStderr)
  1077  		}
  1078  
  1079  		ts.checkCmd(bg)
  1080  	}
  1081  
  1082  	ts.stdout = strings.Join(stdouts, "")
  1083  	ts.stderr = strings.Join(stderrs, "")
  1084  	ts.background = nil
  1085  }
  1086  
  1087  // Helpers for command implementations.
  1088  
  1089  // abbrev abbreviates the actual work directory in the string s to the literal string "$WORK".
  1090  func (ts *testScript) abbrev(s string) string {
  1091  	s = strings.ReplaceAll(s, ts.workdir, "$WORK")
  1092  	if *testWork {
  1093  		// Expose actual $WORK value in environment dump on first line of work script,
  1094  		// so that the user can find out what directory -testwork left behind.
  1095  		s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n")
  1096  	}
  1097  	return s
  1098  }
  1099  
  1100  // check calls ts.fatalf if err != nil.
  1101  func (ts *testScript) check(err error) {
  1102  	if err != nil {
  1103  		ts.fatalf("%v", err)
  1104  	}
  1105  }
  1106  
  1107  func (ts *testScript) checkCmd(bg *backgroundCmd) {
  1108  	select {
  1109  	case <-bg.done:
  1110  	default:
  1111  		panic("checkCmd called when not done")
  1112  	}
  1113  
  1114  	if bg.err == nil {
  1115  		if bg.want == failure {
  1116  			ts.fatalf("unexpected command success")
  1117  		}
  1118  		return
  1119  	}
  1120  
  1121  	if errors.Is(bg.err, context.DeadlineExceeded) {
  1122  		ts.fatalf("test timed out while running command")
  1123  	}
  1124  
  1125  	if errors.Is(bg.err, context.Canceled) {
  1126  		// The process was still running at the end of the test.
  1127  		// The test must not depend on its exit status.
  1128  		if bg.want != successOrFailure {
  1129  			ts.fatalf("unexpected background command remaining at test end")
  1130  		}
  1131  		return
  1132  	}
  1133  
  1134  	if bg.want == success {
  1135  		ts.fatalf("unexpected command failure")
  1136  	}
  1137  }
  1138  
  1139  // exec runs the given command line (an actual subprocess, not simulated)
  1140  // in ts.cd with environment ts.env and then returns collected standard output and standard error.
  1141  func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) {
  1142  	bg, err := ts.startBackground(success, command, args...)
  1143  	if err != nil {
  1144  		return "", "", err
  1145  	}
  1146  	<-bg.done
  1147  	return bg.stdout.String(), bg.stderr.String(), bg.err
  1148  }
  1149  
  1150  // startBackground starts the given command line (an actual subprocess, not simulated)
  1151  // in ts.cd with environment ts.env.
  1152  func (ts *testScript) startBackground(want simpleStatus, command string, args ...string) (*backgroundCmd, error) {
  1153  	done := make(chan struct{})
  1154  	bg := &backgroundCmd{
  1155  		want: want,
  1156  		args: append([]string{command}, args...),
  1157  		done: done,
  1158  	}
  1159  
  1160  	// Use the script's PATH to look up the command if it contains a separator
  1161  	// instead of the test process's PATH (see lookPath).
  1162  	// Don't use filepath.Clean, since that changes "./foo" to "foo".
  1163  	command = filepath.FromSlash(command)
  1164  	if !strings.Contains(command, string(filepath.Separator)) {
  1165  		var err error
  1166  		command, err = ts.lookPath(command)
  1167  		if err != nil {
  1168  			return nil, err
  1169  		}
  1170  	}
  1171  	cmd := exec.Command(command, args...)
  1172  	cmd.Dir = ts.cd
  1173  	cmd.Env = append(ts.env, "PWD="+ts.cd)
  1174  	cmd.Stdout = &bg.stdout
  1175  	cmd.Stderr = &bg.stderr
  1176  	if err := cmd.Start(); err != nil {
  1177  		return nil, err
  1178  	}
  1179  
  1180  	go func() {
  1181  		bg.err = waitOrStop(ts.ctx, cmd, quitSignal(), ts.gracePeriod)
  1182  		close(done)
  1183  	}()
  1184  	return bg, nil
  1185  }
  1186  
  1187  // lookPath is (roughly) like exec.LookPath, but it uses the test script's PATH
  1188  // instead of the test process's PATH to find the executable. We don't change
  1189  // the test process's PATH since it may run scripts in parallel.
  1190  func (ts *testScript) lookPath(command string) (string, error) {
  1191  	var strEqual func(string, string) bool
  1192  	if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
  1193  		// Using GOOS as a proxy for case-insensitive file system.
  1194  		strEqual = strings.EqualFold
  1195  	} else {
  1196  		strEqual = func(a, b string) bool { return a == b }
  1197  	}
  1198  
  1199  	var pathExt []string
  1200  	var searchExt bool
  1201  	var isExecutable func(os.FileInfo) bool
  1202  	if runtime.GOOS == "windows" {
  1203  		// Use the test process's PathExt instead of the script's.
  1204  		// If PathExt is set in the command's environment, cmd.Start fails with
  1205  		// "parameter is invalid". Not sure why.
  1206  		// If the command already has an extension in PathExt (like "cmd.exe")
  1207  		// don't search for other extensions (not "cmd.bat.exe").
  1208  		pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
  1209  		searchExt = true
  1210  		cmdExt := filepath.Ext(command)
  1211  		for _, ext := range pathExt {
  1212  			if strEqual(cmdExt, ext) {
  1213  				searchExt = false
  1214  				break
  1215  			}
  1216  		}
  1217  		isExecutable = func(fi os.FileInfo) bool {
  1218  			return fi.Mode().IsRegular()
  1219  		}
  1220  	} else {
  1221  		isExecutable = func(fi os.FileInfo) bool {
  1222  			return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
  1223  		}
  1224  	}
  1225  
  1226  	pathName := "PATH"
  1227  	if runtime.GOOS == "plan9" {
  1228  		pathName = "path"
  1229  	}
  1230  
  1231  	for _, dir := range strings.Split(ts.envMap[pathName], string(filepath.ListSeparator)) {
  1232  		if searchExt {
  1233  			ents, err := os.ReadDir(dir)
  1234  			if err != nil {
  1235  				continue
  1236  			}
  1237  			for _, ent := range ents {
  1238  				for _, ext := range pathExt {
  1239  					if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
  1240  						return dir + string(filepath.Separator) + ent.Name(), nil
  1241  					}
  1242  				}
  1243  			}
  1244  		} else {
  1245  			path := dir + string(filepath.Separator) + command
  1246  			if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
  1247  				return path, nil
  1248  			}
  1249  		}
  1250  	}
  1251  	return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
  1252  }
  1253  
  1254  // waitOrStop waits for the already-started command cmd by calling its Wait method.
  1255  //
  1256  // If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal.
  1257  // If killDelay is positive, waitOrStop waits that additional period for Wait to return before sending os.Kill.
  1258  //
  1259  // This function is copied from the one added to x/playground/internal in
  1260  // http://golang.org/cl/228438.
  1261  func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error {
  1262  	if cmd.Process == nil {
  1263  		panic("waitOrStop called with a nil cmd.Process — missing Start call?")
  1264  	}
  1265  	if interrupt == nil {
  1266  		panic("waitOrStop requires a non-nil interrupt signal")
  1267  	}
  1268  
  1269  	errc := make(chan error)
  1270  	go func() {
  1271  		select {
  1272  		case errc <- nil:
  1273  			return
  1274  		case <-ctx.Done():
  1275  		}
  1276  
  1277  		err := cmd.Process.Signal(interrupt)
  1278  		if err == nil {
  1279  			err = ctx.Err() // Report ctx.Err() as the reason we interrupted.
  1280  		} else if err == os.ErrProcessDone {
  1281  			errc <- nil
  1282  			return
  1283  		}
  1284  
  1285  		if killDelay > 0 {
  1286  			timer := time.NewTimer(killDelay)
  1287  			select {
  1288  			// Report ctx.Err() as the reason we interrupted the process...
  1289  			case errc <- ctx.Err():
  1290  				timer.Stop()
  1291  				return
  1292  			// ...but after killDelay has elapsed, fall back to a stronger signal.
  1293  			case <-timer.C:
  1294  			}
  1295  
  1296  			// Wait still hasn't returned.
  1297  			// Kill the process harder to make sure that it exits.
  1298  			//
  1299  			// Ignore any error: if cmd.Process has already terminated, we still
  1300  			// want to send ctx.Err() (or the error from the Interrupt call)
  1301  			// to properly attribute the signal that may have terminated it.
  1302  			_ = cmd.Process.Kill()
  1303  		}
  1304  
  1305  		errc <- err
  1306  	}()
  1307  
  1308  	waitErr := cmd.Wait()
  1309  	if interruptErr := <-errc; interruptErr != nil {
  1310  		return interruptErr
  1311  	}
  1312  	return waitErr
  1313  }
  1314  
  1315  // expand applies environment variable expansion to the string s.
  1316  func (ts *testScript) expand(s string, inRegexp bool) string {
  1317  	return os.Expand(s, func(key string) string {
  1318  		e := ts.envMap[key]
  1319  		if inRegexp {
  1320  			// Replace workdir with $WORK, since we have done the same substitution in
  1321  			// the text we're about to compare against.
  1322  			e = strings.ReplaceAll(e, ts.workdir, "$WORK")
  1323  
  1324  			// Quote to literal strings: we want paths like C:\work\go1.4 to remain
  1325  			// paths rather than regular expressions.
  1326  			e = regexp.QuoteMeta(e)
  1327  		}
  1328  		return e
  1329  	})
  1330  }
  1331  
  1332  // fatalf aborts the test with the given failure message.
  1333  func (ts *testScript) fatalf(format string, args ...any) {
  1334  	fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
  1335  	ts.t.FailNow()
  1336  }
  1337  
  1338  // mkabs interprets file relative to the test script's current directory
  1339  // and returns the corresponding absolute path.
  1340  func (ts *testScript) mkabs(file string) string {
  1341  	if filepath.IsAbs(file) {
  1342  		return file
  1343  	}
  1344  	return filepath.Join(ts.cd, file)
  1345  }
  1346  
  1347  // A condition guards execution of a command.
  1348  type condition struct {
  1349  	want bool
  1350  	tag  string
  1351  }
  1352  
  1353  // A command is a complete command parsed from a script.
  1354  type command struct {
  1355  	want  simpleStatus
  1356  	conds []condition // all must be satisfied
  1357  	name  string      // the name of the command; must be non-empty
  1358  	args  []string    // shell-expanded arguments following name
  1359  }
  1360  
  1361  // parse parses a single line as a list of space-separated arguments
  1362  // subject to environment variable expansion (but not resplitting).
  1363  // Single quotes around text disable splitting and expansion.
  1364  // To embed a single quote, double it: 'Don''t communicate by sharing memory.'
  1365  func (ts *testScript) parse(line string) command {
  1366  	ts.line = line
  1367  
  1368  	var (
  1369  		cmd      command
  1370  		arg      string  // text of current arg so far (need to add line[start:i])
  1371  		start    = -1    // if >= 0, position where current arg text chunk starts
  1372  		quoted   = false // currently processing quoted text
  1373  		isRegexp = false // currently processing unquoted regular expression
  1374  	)
  1375  
  1376  	flushArg := func() {
  1377  		defer func() {
  1378  			arg = ""
  1379  			start = -1
  1380  		}()
  1381  
  1382  		if cmd.name != "" {
  1383  			cmd.args = append(cmd.args, arg)
  1384  			// Commands take only one regexp argument (after the optional flags),
  1385  			// so no subsequent args are regexps. Liberally assume an argument that
  1386  			// starts with a '-' is a flag.
  1387  			if len(arg) == 0 || arg[0] != '-' {
  1388  				isRegexp = false
  1389  			}
  1390  			return
  1391  		}
  1392  
  1393  		// Command prefix ! means negate the expectations about this command:
  1394  		// go command should fail, match should not be found, etc.
  1395  		// Prefix ? means allow either success or failure.
  1396  		switch want := simpleStatus(arg); want {
  1397  		case failure, successOrFailure:
  1398  			if cmd.want != "" {
  1399  				ts.fatalf("duplicated '!' or '?' token")
  1400  			}
  1401  			cmd.want = want
  1402  			return
  1403  		}
  1404  
  1405  		// Command prefix [cond] means only run this command if cond is satisfied.
  1406  		if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
  1407  			want := true
  1408  			arg = strings.TrimSpace(arg[1 : len(arg)-1])
  1409  			if strings.HasPrefix(arg, "!") {
  1410  				want = false
  1411  				arg = strings.TrimSpace(arg[1:])
  1412  			}
  1413  			if arg == "" {
  1414  				ts.fatalf("empty condition")
  1415  			}
  1416  			cmd.conds = append(cmd.conds, condition{want: want, tag: arg})
  1417  			return
  1418  		}
  1419  
  1420  		cmd.name = arg
  1421  		isRegexp = regexpCmd[cmd.name]
  1422  	}
  1423  
  1424  	for i := 0; ; i++ {
  1425  		if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
  1426  			// Found arg-separating space.
  1427  			if start >= 0 {
  1428  				arg += ts.expand(line[start:i], isRegexp)
  1429  				flushArg()
  1430  			}
  1431  			if i >= len(line) || line[i] == '#' {
  1432  				break
  1433  			}
  1434  			continue
  1435  		}
  1436  		if i >= len(line) {
  1437  			ts.fatalf("unterminated quoted argument")
  1438  		}
  1439  		if line[i] == '\'' {
  1440  			if !quoted {
  1441  				// starting a quoted chunk
  1442  				if start >= 0 {
  1443  					arg += ts.expand(line[start:i], isRegexp)
  1444  				}
  1445  				start = i + 1
  1446  				quoted = true
  1447  				continue
  1448  			}
  1449  			// 'foo''bar' means foo'bar, like in rc shell and Pascal.
  1450  			if i+1 < len(line) && line[i+1] == '\'' {
  1451  				arg += line[start:i]
  1452  				start = i + 1
  1453  				i++ // skip over second ' before next iteration
  1454  				continue
  1455  			}
  1456  			// ending a quoted chunk
  1457  			arg += line[start:i]
  1458  			start = i + 1
  1459  			quoted = false
  1460  			continue
  1461  		}
  1462  		// found character worth saving; make sure we're saving
  1463  		if start < 0 {
  1464  			start = i
  1465  		}
  1466  	}
  1467  	return cmd
  1468  }
  1469  
  1470  // updateSum runs 'go mod tidy', 'go list -mod=mod -m all', or
  1471  // 'go list -mod=mod all' in the test's current directory if a file named
  1472  // "go.mod" is present after the archive has been extracted. updateSum modifies
  1473  // archive and returns true if go.mod or go.sum were changed.
  1474  func (ts *testScript) updateSum(archive *txtar.Archive) (rewrite bool) {
  1475  	gomodIdx, gosumIdx := -1, -1
  1476  	for i := range archive.Files {
  1477  		switch archive.Files[i].Name {
  1478  		case "go.mod":
  1479  			gomodIdx = i
  1480  		case "go.sum":
  1481  			gosumIdx = i
  1482  		}
  1483  	}
  1484  	if gomodIdx < 0 {
  1485  		return false
  1486  	}
  1487  
  1488  	switch *testSum {
  1489  	case "tidy":
  1490  		ts.cmdGo(success, []string{"mod", "tidy"})
  1491  	case "listm":
  1492  		ts.cmdGo(success, []string{"list", "-m", "-mod=mod", "all"})
  1493  	case "listall":
  1494  		ts.cmdGo(success, []string{"list", "-mod=mod", "all"})
  1495  	default:
  1496  		ts.t.Fatalf(`unknown value for -testsum %q; may be "tidy", "listm", or "listall"`, *testSum)
  1497  	}
  1498  
  1499  	newGomodData, err := os.ReadFile(filepath.Join(ts.cd, "go.mod"))
  1500  	if err != nil {
  1501  		ts.t.Fatalf("reading go.mod after -testsum: %v", err)
  1502  	}
  1503  	if !bytes.Equal(newGomodData, archive.Files[gomodIdx].Data) {
  1504  		archive.Files[gomodIdx].Data = newGomodData
  1505  		rewrite = true
  1506  	}
  1507  
  1508  	newGosumData, err := os.ReadFile(filepath.Join(ts.cd, "go.sum"))
  1509  	if err != nil && !os.IsNotExist(err) {
  1510  		ts.t.Fatalf("reading go.sum after -testsum: %v", err)
  1511  	}
  1512  	switch {
  1513  	case os.IsNotExist(err) && gosumIdx >= 0:
  1514  		// go.sum was deleted.
  1515  		rewrite = true
  1516  		archive.Files = append(archive.Files[:gosumIdx], archive.Files[gosumIdx+1:]...)
  1517  	case err == nil && gosumIdx < 0:
  1518  		// go.sum was created.
  1519  		rewrite = true
  1520  		gosumIdx = gomodIdx + 1
  1521  		archive.Files = append(archive.Files, txtar.File{})
  1522  		copy(archive.Files[gosumIdx+1:], archive.Files[gosumIdx:])
  1523  		archive.Files[gosumIdx] = txtar.File{Name: "go.sum", Data: newGosumData}
  1524  	case err == nil && gosumIdx >= 0 && !bytes.Equal(newGosumData, archive.Files[gosumIdx].Data):
  1525  		// go.sum was changed.
  1526  		rewrite = true
  1527  		archive.Files[gosumIdx].Data = newGosumData
  1528  	}
  1529  	return rewrite
  1530  }
  1531  
  1532  // diff returns a formatted diff of the two texts,
  1533  // showing the entire text and the minimum line-level
  1534  // additions and removals to turn text1 into text2.
  1535  // (That is, lines only in text1 appear with a leading -,
  1536  // and lines only in text2 appear with a leading +.)
  1537  func diff(text1, text2 string) string {
  1538  	if text1 != "" && !strings.HasSuffix(text1, "\n") {
  1539  		text1 += "(missing final newline)"
  1540  	}
  1541  	lines1 := strings.Split(text1, "\n")
  1542  	lines1 = lines1[:len(lines1)-1] // remove empty string after final line
  1543  	if text2 != "" && !strings.HasSuffix(text2, "\n") {
  1544  		text2 += "(missing final newline)"
  1545  	}
  1546  	lines2 := strings.Split(text2, "\n")
  1547  	lines2 = lines2[:len(lines2)-1] // remove empty string after final line
  1548  
  1549  	// Naive dynamic programming algorithm for edit distance.
  1550  	// https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm
  1551  	// dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j]
  1552  	// (The reversed indices make following the minimum cost path
  1553  	// visit lines in the same order as in the text.)
  1554  	dist := make([][]int, len(lines1)+1)
  1555  	for i := range dist {
  1556  		dist[i] = make([]int, len(lines2)+1)
  1557  		if i == 0 {
  1558  			for j := range dist[0] {
  1559  				dist[0][j] = j
  1560  			}
  1561  			continue
  1562  		}
  1563  		for j := range dist[i] {
  1564  			if j == 0 {
  1565  				dist[i][0] = i
  1566  				continue
  1567  			}
  1568  			cost := dist[i][j-1] + 1
  1569  			if cost > dist[i-1][j]+1 {
  1570  				cost = dist[i-1][j] + 1
  1571  			}
  1572  			if lines1[len(lines1)-i] == lines2[len(lines2)-j] {
  1573  				if cost > dist[i-1][j-1] {
  1574  					cost = dist[i-1][j-1]
  1575  				}
  1576  			}
  1577  			dist[i][j] = cost
  1578  		}
  1579  	}
  1580  
  1581  	var buf strings.Builder
  1582  	i, j := len(lines1), len(lines2)
  1583  	for i > 0 || j > 0 {
  1584  		cost := dist[i][j]
  1585  		if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] {
  1586  			fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i])
  1587  			i--
  1588  			j--
  1589  		} else if i > 0 && cost == dist[i-1][j]+1 {
  1590  			fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i])
  1591  			i--
  1592  		} else {
  1593  			fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j])
  1594  			j--
  1595  		}
  1596  	}
  1597  	return buf.String()
  1598  }
  1599  
  1600  var diffTests = []struct {
  1601  	text1 string
  1602  	text2 string
  1603  	diff  string
  1604  }{
  1605  	{"a b c", "a b d e f", "a b -c +d +e +f"},
  1606  	{"", "a b c", "+a +b +c"},
  1607  	{"a b c", "", "-a -b -c"},
  1608  	{"a b c", "d e f", "-a -b -c +d +e +f"},
  1609  	{"a b c d e f", "a b d e f", "a b -c d e f"},
  1610  	{"a b c e f", "a b c d e f", "a b c +d e f"},
  1611  }
  1612  
  1613  func TestDiff(t *testing.T) {
  1614  	t.Parallel()
  1615  
  1616  	for _, tt := range diffTests {
  1617  		// Turn spaces into \n.
  1618  		text1 := strings.ReplaceAll(tt.text1, " ", "\n")
  1619  		if text1 != "" {
  1620  			text1 += "\n"
  1621  		}
  1622  		text2 := strings.ReplaceAll(tt.text2, " ", "\n")
  1623  		if text2 != "" {
  1624  			text2 += "\n"
  1625  		}
  1626  		out := diff(text1, text2)
  1627  		// Cut final \n, cut spaces, turn remaining \n into spaces.
  1628  		out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ")
  1629  		if out != tt.diff {
  1630  			t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff)
  1631  		}
  1632  	}
  1633  }
  1634  

View as plain text