Black Lives Matter. Support the Equal Justice Initiative.

Source file src/cmd/go/script_test.go

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

View as plain text