github.com/hikaru7719/go@v0.0.0-20181025140707-c8b2ac68906a/src/cmd/go/script_test.go (about)

     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  	"fmt"
    13  	"internal/testenv"
    14  	"io/ioutil"
    15  	"os"
    16  	"os/exec"
    17  	"path/filepath"
    18  	"regexp"
    19  	"runtime"
    20  	"strconv"
    21  	"strings"
    22  	"testing"
    23  	"time"
    24  
    25  	"cmd/go/internal/imports"
    26  	"cmd/go/internal/par"
    27  	"cmd/go/internal/txtar"
    28  )
    29  
    30  // TestScript runs the tests in testdata/script/*.txt.
    31  func TestScript(t *testing.T) {
    32  	testenv.MustHaveGoBuild(t)
    33  	if skipExternal {
    34  		t.Skipf("skipping external tests on %s/%s", runtime.GOOS, runtime.GOARCH)
    35  	}
    36  
    37  	files, err := filepath.Glob("testdata/script/*.txt")
    38  	if err != nil {
    39  		t.Fatal(err)
    40  	}
    41  	for _, file := range files {
    42  		file := file
    43  		name := strings.TrimSuffix(filepath.Base(file), ".txt")
    44  		t.Run(name, func(t *testing.T) {
    45  			t.Parallel()
    46  			ts := &testScript{t: t, name: name, file: file}
    47  			ts.setup()
    48  			if !*testWork {
    49  				defer removeAll(ts.workdir)
    50  			}
    51  			ts.run()
    52  		})
    53  	}
    54  }
    55  
    56  // A testScript holds execution state for a single test script.
    57  type testScript struct {
    58  	t       *testing.T
    59  	workdir string            // temporary work dir ($WORK)
    60  	log     bytes.Buffer      // test execution log (printed at end of test)
    61  	mark    int               // offset of next log truncation
    62  	cd      string            // current directory during test execution; initially $WORK/gopath/src
    63  	name    string            // short name of test ("foo")
    64  	file    string            // full file name ("testdata/script/foo.txt")
    65  	lineno  int               // line number currently executing
    66  	line    string            // line currently executing
    67  	env     []string          // environment list (for os/exec)
    68  	envMap  map[string]string // environment mapping (matches env)
    69  	stdout  string            // standard output from last 'go' command; for 'stdout' command
    70  	stderr  string            // standard error from last 'go' command; for 'stderr' command
    71  	stopped bool              // test wants to stop early
    72  	start   time.Time         // time phase started
    73  }
    74  
    75  var extraEnvKeys = []string{
    76  	"SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210
    77  }
    78  
    79  // setup sets up the test execution temporary directory and environment.
    80  func (ts *testScript) setup() {
    81  	StartProxy()
    82  	ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name)
    83  	ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777))
    84  	ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777))
    85  	ts.cd = filepath.Join(ts.workdir, "gopath/src")
    86  	ts.env = []string{
    87  		"WORK=" + ts.workdir, // must be first for ts.abbrev
    88  		"PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"),
    89  		homeEnvName() + "=/no-home",
    90  		"CCACHE_DISABLE=1", // ccache breaks with non-existent HOME
    91  		"GOARCH=" + runtime.GOARCH,
    92  		"GOCACHE=" + testGOCACHE,
    93  		"GOOS=" + runtime.GOOS,
    94  		"GOPATH=" + filepath.Join(ts.workdir, "gopath"),
    95  		"GOPROXY=" + proxyURL,
    96  		"GOROOT=" + testGOROOT,
    97  		tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"),
    98  		"devnull=" + os.DevNull,
    99  		":=" + string(os.PathListSeparator),
   100  	}
   101  
   102  	if runtime.GOOS == "plan9" {
   103  		ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path"))
   104  	}
   105  
   106  	if runtime.GOOS == "windows" {
   107  		ts.env = append(ts.env, "exe=.exe")
   108  	} else {
   109  		ts.env = append(ts.env, "exe=")
   110  	}
   111  	for _, key := range extraEnvKeys {
   112  		if val := os.Getenv(key); val != "" {
   113  			ts.env = append(ts.env, key+"="+val)
   114  		}
   115  	}
   116  
   117  	ts.envMap = make(map[string]string)
   118  	for _, kv := range ts.env {
   119  		if i := strings.Index(kv, "="); i >= 0 {
   120  			ts.envMap[kv[:i]] = kv[i+1:]
   121  		}
   122  	}
   123  }
   124  
   125  var execCache par.Cache
   126  
   127  // run runs the test script.
   128  func (ts *testScript) run() {
   129  	// Truncate log at end of last phase marker,
   130  	// discarding details of successful phase.
   131  	rewind := func() {
   132  		if !testing.Verbose() {
   133  			ts.log.Truncate(ts.mark)
   134  		}
   135  	}
   136  
   137  	// Insert elapsed time for phase at end of phase marker
   138  	markTime := func() {
   139  		if ts.mark > 0 && !ts.start.IsZero() {
   140  			afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
   141  			ts.log.Truncate(ts.mark - 1) // cut \n and afterMark
   142  			fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds())
   143  			ts.log.Write(afterMark)
   144  		}
   145  		ts.start = time.Time{}
   146  	}
   147  
   148  	defer func() {
   149  		markTime()
   150  		// Flush testScript log to testing.T log.
   151  		ts.t.Log("\n" + ts.abbrev(ts.log.String()))
   152  	}()
   153  
   154  	// Unpack archive.
   155  	a, err := txtar.ParseFile(ts.file)
   156  	ts.check(err)
   157  	for _, f := range a.Files {
   158  		name := ts.mkabs(ts.expand(f.Name))
   159  		ts.check(os.MkdirAll(filepath.Dir(name), 0777))
   160  		ts.check(ioutil.WriteFile(name, f.Data, 0666))
   161  	}
   162  
   163  	// With -v or -testwork, start log with full environment.
   164  	if *testWork || testing.Verbose() {
   165  		// Display environment.
   166  		ts.cmdEnv(false, nil)
   167  		fmt.Fprintf(&ts.log, "\n")
   168  		ts.mark = ts.log.Len()
   169  	}
   170  
   171  	// Run script.
   172  	// See testdata/script/README for documentation of script form.
   173  	script := string(a.Comment)
   174  Script:
   175  	for script != "" {
   176  		// Extract next line.
   177  		ts.lineno++
   178  		var line string
   179  		if i := strings.Index(script, "\n"); i >= 0 {
   180  			line, script = script[:i], script[i+1:]
   181  		} else {
   182  			line, script = script, ""
   183  		}
   184  
   185  		// # is a comment indicating the start of new phase.
   186  		if strings.HasPrefix(line, "#") {
   187  			// If there was a previous phase, it succeeded,
   188  			// so rewind the log to delete its details (unless -v is in use).
   189  			// If nothing has happened at all since the mark,
   190  			// rewinding is a no-op and adding elapsed time
   191  			// for doing nothing is meaningless, so don't.
   192  			if ts.log.Len() > ts.mark {
   193  				rewind()
   194  				markTime()
   195  			}
   196  			// Print phase heading and mark start of phase output.
   197  			fmt.Fprintf(&ts.log, "%s\n", line)
   198  			ts.mark = ts.log.Len()
   199  			ts.start = time.Now()
   200  			continue
   201  		}
   202  
   203  		// Parse input line. Ignore blanks entirely.
   204  		args := ts.parse(line)
   205  		if len(args) == 0 {
   206  			continue
   207  		}
   208  
   209  		// Echo command to log.
   210  		fmt.Fprintf(&ts.log, "> %s\n", line)
   211  
   212  		// Command prefix [cond] means only run this command if cond is satisfied.
   213  		for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
   214  			cond := args[0]
   215  			cond = cond[1 : len(cond)-1]
   216  			cond = strings.TrimSpace(cond)
   217  			args = args[1:]
   218  			if len(args) == 0 {
   219  				ts.fatalf("missing command after condition")
   220  			}
   221  			want := true
   222  			if strings.HasPrefix(cond, "!") {
   223  				want = false
   224  				cond = strings.TrimSpace(cond[1:])
   225  			}
   226  			// Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short).
   227  			//
   228  			// NOTE: If you make changes here, update testdata/script/README too!
   229  			//
   230  			ok := false
   231  			switch cond {
   232  			case runtime.GOOS, runtime.GOARCH, runtime.Compiler:
   233  				ok = true
   234  			case "short":
   235  				ok = testing.Short()
   236  			case "cgo":
   237  				ok = canCgo
   238  			case "msan":
   239  				ok = canMSan
   240  			case "race":
   241  				ok = canRace
   242  			case "net":
   243  				ok = testenv.HasExternalNetwork()
   244  			case "link":
   245  				ok = testenv.HasLink()
   246  			case "symlink":
   247  				ok = testenv.HasSymlink()
   248  			default:
   249  				if strings.HasPrefix(cond, "exec:") {
   250  					prog := cond[len("exec:"):]
   251  					ok = execCache.Do(prog, func() interface{} {
   252  						_, err := exec.LookPath(prog)
   253  						return err == nil
   254  					}).(bool)
   255  					break
   256  				}
   257  				if !imports.KnownArch[cond] && !imports.KnownOS[cond] && cond != "gc" && cond != "gccgo" {
   258  					ts.fatalf("unknown condition %q", cond)
   259  				}
   260  			}
   261  			if ok != want {
   262  				// Don't run rest of line.
   263  				continue Script
   264  			}
   265  		}
   266  
   267  		// Command prefix ! means negate the expectations about this command:
   268  		// go command should fail, match should not be found, etc.
   269  		neg := false
   270  		if args[0] == "!" {
   271  			neg = true
   272  			args = args[1:]
   273  			if len(args) == 0 {
   274  				ts.fatalf("! on line by itself")
   275  			}
   276  		}
   277  
   278  		// Run command.
   279  		cmd := scriptCmds[args[0]]
   280  		if cmd == nil {
   281  			ts.fatalf("unknown command %q", args[0])
   282  		}
   283  		cmd(ts, neg, args[1:])
   284  
   285  		// Command can ask script to stop early.
   286  		if ts.stopped {
   287  			return
   288  		}
   289  	}
   290  
   291  	// Final phase ended.
   292  	rewind()
   293  	markTime()
   294  	fmt.Fprintf(&ts.log, "PASS\n")
   295  }
   296  
   297  // scriptCmds are the script command implementations.
   298  // Keep list and the implementations below sorted by name.
   299  //
   300  // NOTE: If you make changes here, update testdata/script/README too!
   301  //
   302  var scriptCmds = map[string]func(*testScript, bool, []string){
   303  	"addcrlf": (*testScript).cmdAddcrlf,
   304  	"cd":      (*testScript).cmdCd,
   305  	"cmp":     (*testScript).cmdCmp,
   306  	"cp":      (*testScript).cmdCp,
   307  	"env":     (*testScript).cmdEnv,
   308  	"exec":    (*testScript).cmdExec,
   309  	"exists":  (*testScript).cmdExists,
   310  	"go":      (*testScript).cmdGo,
   311  	"grep":    (*testScript).cmdGrep,
   312  	"mkdir":   (*testScript).cmdMkdir,
   313  	"rm":      (*testScript).cmdRm,
   314  	"skip":    (*testScript).cmdSkip,
   315  	"stale":   (*testScript).cmdStale,
   316  	"stderr":  (*testScript).cmdStderr,
   317  	"stdout":  (*testScript).cmdStdout,
   318  	"stop":    (*testScript).cmdStop,
   319  	"symlink": (*testScript).cmdSymlink,
   320  }
   321  
   322  // addcrlf adds CRLF line endings to the named files.
   323  func (ts *testScript) cmdAddcrlf(neg bool, args []string) {
   324  	if len(args) == 0 {
   325  		ts.fatalf("usage: addcrlf file...")
   326  	}
   327  
   328  	for _, file := range args {
   329  		file = ts.mkabs(file)
   330  		data, err := ioutil.ReadFile(file)
   331  		ts.check(err)
   332  		ts.check(ioutil.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666))
   333  	}
   334  }
   335  
   336  // cd changes to a different directory.
   337  func (ts *testScript) cmdCd(neg bool, args []string) {
   338  	if neg {
   339  		ts.fatalf("unsupported: ! cd")
   340  	}
   341  	if len(args) != 1 {
   342  		ts.fatalf("usage: cd dir")
   343  	}
   344  
   345  	dir := args[0]
   346  	if !filepath.IsAbs(dir) {
   347  		dir = filepath.Join(ts.cd, dir)
   348  	}
   349  	info, err := os.Stat(dir)
   350  	if os.IsNotExist(err) {
   351  		ts.fatalf("directory %s does not exist", dir)
   352  	}
   353  	ts.check(err)
   354  	if !info.IsDir() {
   355  		ts.fatalf("%s is not a directory", dir)
   356  	}
   357  	ts.cd = dir
   358  	fmt.Fprintf(&ts.log, "%s\n", ts.cd)
   359  }
   360  
   361  // cmp compares two files.
   362  func (ts *testScript) cmdCmp(neg bool, args []string) {
   363  	if neg {
   364  		// It would be strange to say "this file can have any content except this precise byte sequence".
   365  		ts.fatalf("unsupported: ! cmp")
   366  	}
   367  	if len(args) != 2 {
   368  		ts.fatalf("usage: cmp file1 file2")
   369  	}
   370  
   371  	name1, name2 := args[0], args[1]
   372  	var text1, text2 string
   373  	if name1 == "stdout" {
   374  		text1 = ts.stdout
   375  	} else if name1 == "stderr" {
   376  		text1 = ts.stderr
   377  	} else {
   378  		data, err := ioutil.ReadFile(ts.mkabs(name1))
   379  		ts.check(err)
   380  		text1 = string(data)
   381  	}
   382  
   383  	data, err := ioutil.ReadFile(ts.mkabs(name2))
   384  	ts.check(err)
   385  	text2 = string(data)
   386  
   387  	if text1 == text2 {
   388  		return
   389  	}
   390  
   391  	fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2))
   392  	ts.fatalf("%s and %s differ", name1, name2)
   393  }
   394  
   395  // cp copies files, maybe eventually directories.
   396  func (ts *testScript) cmdCp(neg bool, args []string) {
   397  	if neg {
   398  		ts.fatalf("unsupported: ! cp")
   399  	}
   400  	if len(args) < 2 {
   401  		ts.fatalf("usage: cp src... dst")
   402  	}
   403  
   404  	dst := ts.mkabs(args[len(args)-1])
   405  	info, err := os.Stat(dst)
   406  	dstDir := err == nil && info.IsDir()
   407  	if len(args) > 2 && !dstDir {
   408  		ts.fatalf("cp: destination %s is not a directory", dst)
   409  	}
   410  
   411  	for _, arg := range args[:len(args)-1] {
   412  		src := ts.mkabs(arg)
   413  		info, err := os.Stat(src)
   414  		ts.check(err)
   415  		data, err := ioutil.ReadFile(src)
   416  		ts.check(err)
   417  		targ := dst
   418  		if dstDir {
   419  			targ = filepath.Join(dst, filepath.Base(src))
   420  		}
   421  		ts.check(ioutil.WriteFile(targ, data, info.Mode()&0777))
   422  	}
   423  }
   424  
   425  // env displays or adds to the environment.
   426  func (ts *testScript) cmdEnv(neg bool, args []string) {
   427  	if neg {
   428  		ts.fatalf("unsupported: ! env")
   429  	}
   430  	if len(args) == 0 {
   431  		printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once
   432  		for _, kv := range ts.env {
   433  			k := kv[:strings.Index(kv, "=")]
   434  			if !printed[k] {
   435  				fmt.Fprintf(&ts.log, "%s=%s\n", k, ts.envMap[k])
   436  			}
   437  		}
   438  		return
   439  	}
   440  	for _, env := range args {
   441  		i := strings.Index(env, "=")
   442  		if i < 0 {
   443  			// Display value instead of setting it.
   444  			fmt.Fprintf(&ts.log, "%s=%s\n", env, ts.envMap[env])
   445  			continue
   446  		}
   447  		ts.env = append(ts.env, env)
   448  		ts.envMap[env[:i]] = env[i+1:]
   449  	}
   450  }
   451  
   452  // exec runs the given command.
   453  func (ts *testScript) cmdExec(neg bool, args []string) {
   454  	if len(args) < 1 {
   455  		ts.fatalf("usage: exec program [args...]")
   456  	}
   457  	var err error
   458  	ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
   459  	if ts.stdout != "" {
   460  		fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
   461  	}
   462  	if ts.stderr != "" {
   463  		fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
   464  	}
   465  	if err != nil {
   466  		fmt.Fprintf(&ts.log, "[%v]\n", err)
   467  		if !neg {
   468  			ts.fatalf("unexpected command failure")
   469  		}
   470  	} else {
   471  		if neg {
   472  			ts.fatalf("unexpected command success")
   473  		}
   474  	}
   475  }
   476  
   477  // exists checks that the list of files exists.
   478  func (ts *testScript) cmdExists(neg bool, args []string) {
   479  	var readonly bool
   480  	if len(args) > 0 && args[0] == "-readonly" {
   481  		readonly = true
   482  		args = args[1:]
   483  	}
   484  	if len(args) == 0 {
   485  		ts.fatalf("usage: exists [-readonly] file...")
   486  	}
   487  
   488  	for _, file := range args {
   489  		file = ts.mkabs(file)
   490  		info, err := os.Stat(file)
   491  		if err == nil && neg {
   492  			what := "file"
   493  			if info.IsDir() {
   494  				what = "directory"
   495  			}
   496  			ts.fatalf("%s %s unexpectedly exists", what, file)
   497  		}
   498  		if err != nil && !neg {
   499  			ts.fatalf("%s does not exist", file)
   500  		}
   501  		if err == nil && !neg && readonly && info.Mode()&0222 != 0 {
   502  			ts.fatalf("%s exists but is writable", file)
   503  		}
   504  	}
   505  }
   506  
   507  // go runs the go command.
   508  func (ts *testScript) cmdGo(neg bool, args []string) {
   509  	ts.cmdExec(neg, append([]string{testGo}, args...))
   510  }
   511  
   512  // mkdir creates directories.
   513  func (ts *testScript) cmdMkdir(neg bool, args []string) {
   514  	if neg {
   515  		ts.fatalf("unsupported: ! mkdir")
   516  	}
   517  	if len(args) < 1 {
   518  		ts.fatalf("usage: mkdir dir...")
   519  	}
   520  	for _, arg := range args {
   521  		ts.check(os.MkdirAll(ts.mkabs(arg), 0777))
   522  	}
   523  }
   524  
   525  // rm removes files or directories.
   526  func (ts *testScript) cmdRm(neg bool, args []string) {
   527  	if neg {
   528  		ts.fatalf("unsupported: ! rm")
   529  	}
   530  	if len(args) < 1 {
   531  		ts.fatalf("usage: rm file...")
   532  	}
   533  	for _, arg := range args {
   534  		file := ts.mkabs(arg)
   535  		removeAll(file)              // does chmod and then attempts rm
   536  		ts.check(os.RemoveAll(file)) // report error
   537  	}
   538  }
   539  
   540  // skip marks the test skipped.
   541  func (ts *testScript) cmdSkip(neg bool, args []string) {
   542  	if len(args) > 1 {
   543  		ts.fatalf("usage: skip [msg]")
   544  	}
   545  	if neg {
   546  		ts.fatalf("unsupported: ! skip")
   547  	}
   548  	if len(args) == 1 {
   549  		ts.t.Skip(args[0])
   550  	}
   551  	ts.t.Skip()
   552  }
   553  
   554  // stale checks that the named build targets are stale.
   555  func (ts *testScript) cmdStale(neg bool, args []string) {
   556  	if len(args) == 0 {
   557  		ts.fatalf("usage: stale target...")
   558  	}
   559  	tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{else}}"
   560  	if neg {
   561  		tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale{{end}}"
   562  	} else {
   563  		tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}"
   564  	}
   565  	tmpl += "{{end}}"
   566  	goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...)
   567  	stdout, stderr, err := ts.exec(testGo, goArgs...)
   568  	if err != nil {
   569  		ts.fatalf("go list: %v\n%s%s", err, stdout, stderr)
   570  	}
   571  	if stdout != "" {
   572  		ts.fatalf("%s", stdout)
   573  	}
   574  }
   575  
   576  // stdout checks that the last go command standard output matches a regexp.
   577  func (ts *testScript) cmdStdout(neg bool, args []string) {
   578  	scriptMatch(ts, neg, args, ts.stdout, "stdout")
   579  }
   580  
   581  // stderr checks that the last go command standard output matches a regexp.
   582  func (ts *testScript) cmdStderr(neg bool, args []string) {
   583  	scriptMatch(ts, neg, args, ts.stderr, "stderr")
   584  }
   585  
   586  // grep checks that file content matches a regexp.
   587  // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
   588  func (ts *testScript) cmdGrep(neg bool, args []string) {
   589  	scriptMatch(ts, neg, args, "", "grep")
   590  }
   591  
   592  // scriptMatch implements both stdout and stderr.
   593  func scriptMatch(ts *testScript, neg bool, args []string, text, name string) {
   594  	n := 0
   595  	if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
   596  		if neg {
   597  			ts.fatalf("cannot use -count= with negated match")
   598  		}
   599  		var err error
   600  		n, err = strconv.Atoi(args[0][len("-count="):])
   601  		if err != nil {
   602  			ts.fatalf("bad -count=: %v", err)
   603  		}
   604  		if n < 1 {
   605  			ts.fatalf("bad -count=: must be at least 1")
   606  		}
   607  		args = args[1:]
   608  	}
   609  
   610  	extraUsage := ""
   611  	want := 1
   612  	if name == "grep" {
   613  		extraUsage = " file"
   614  		want = 2
   615  	}
   616  	if len(args) != want {
   617  		ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
   618  	}
   619  
   620  	pattern := args[0]
   621  	re, err := regexp.Compile(`(?m)` + pattern)
   622  	ts.check(err)
   623  
   624  	isGrep := name == "grep"
   625  	if isGrep {
   626  		name = args[1] // for error messages
   627  		data, err := ioutil.ReadFile(ts.mkabs(args[1]))
   628  		ts.check(err)
   629  		text = string(data)
   630  	}
   631  
   632  	// Matching against workdir would be misleading.
   633  	text = strings.ReplaceAll(text, ts.workdir, "$WORK")
   634  
   635  	if neg {
   636  		if re.MatchString(text) {
   637  			if isGrep {
   638  				fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
   639  			}
   640  			ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
   641  		}
   642  	} else {
   643  		if !re.MatchString(text) {
   644  			if isGrep {
   645  				fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
   646  			}
   647  			ts.fatalf("no match for %#q found in %s", pattern, name)
   648  		}
   649  		if n > 0 {
   650  			count := len(re.FindAllString(text, -1))
   651  			if count != n {
   652  				if isGrep {
   653  					fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
   654  				}
   655  				ts.fatalf("have %d matches for %#q, want %d", count, pattern, n)
   656  			}
   657  		}
   658  	}
   659  }
   660  
   661  // stop stops execution of the test (marking it passed).
   662  func (ts *testScript) cmdStop(neg bool, args []string) {
   663  	if neg {
   664  		ts.fatalf("unsupported: ! stop")
   665  	}
   666  	if len(args) > 1 {
   667  		ts.fatalf("usage: stop [msg]")
   668  	}
   669  	if len(args) == 1 {
   670  		fmt.Fprintf(&ts.log, "stop: %s\n", args[0])
   671  	} else {
   672  		fmt.Fprintf(&ts.log, "stop\n")
   673  	}
   674  	ts.stopped = true
   675  }
   676  
   677  // symlink creates a symbolic link.
   678  func (ts *testScript) cmdSymlink(neg bool, args []string) {
   679  	if neg {
   680  		ts.fatalf("unsupported: ! symlink")
   681  	}
   682  	if len(args) != 3 || args[1] != "->" {
   683  		ts.fatalf("usage: symlink file -> target")
   684  	}
   685  	// Note that the link target args[2] is not interpreted with mkabs:
   686  	// it will be interpreted relative to the directory file is in.
   687  	ts.check(os.Symlink(args[2], ts.mkabs(args[0])))
   688  }
   689  
   690  // Helpers for command implementations.
   691  
   692  // abbrev abbreviates the actual work directory in the string s to the literal string "$WORK".
   693  func (ts *testScript) abbrev(s string) string {
   694  	s = strings.ReplaceAll(s, ts.workdir, "$WORK")
   695  	if *testWork {
   696  		// Expose actual $WORK value in environment dump on first line of work script,
   697  		// so that the user can find out what directory -testwork left behind.
   698  		s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n")
   699  	}
   700  	return s
   701  }
   702  
   703  // check calls ts.fatalf if err != nil.
   704  func (ts *testScript) check(err error) {
   705  	if err != nil {
   706  		ts.fatalf("%v", err)
   707  	}
   708  }
   709  
   710  // exec runs the given command line (an actual subprocess, not simulated)
   711  // in ts.cd with environment ts.env and then returns collected standard output and standard error.
   712  func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) {
   713  	cmd := exec.Command(command, args...)
   714  	cmd.Dir = ts.cd
   715  	cmd.Env = append(ts.env, "PWD="+ts.cd)
   716  	var stdoutBuf, stderrBuf strings.Builder
   717  	cmd.Stdout = &stdoutBuf
   718  	cmd.Stderr = &stderrBuf
   719  	err = cmd.Run()
   720  	return stdoutBuf.String(), stderrBuf.String(), err
   721  }
   722  
   723  // expand applies environment variable expansion to the string s.
   724  func (ts *testScript) expand(s string) string {
   725  	return os.Expand(s, func(key string) string { return ts.envMap[key] })
   726  }
   727  
   728  // fatalf aborts the test with the given failure message.
   729  func (ts *testScript) fatalf(format string, args ...interface{}) {
   730  	fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
   731  	ts.t.FailNow()
   732  }
   733  
   734  // mkabs interprets file relative to the test script's current directory
   735  // and returns the corresponding absolute path.
   736  func (ts *testScript) mkabs(file string) string {
   737  	if filepath.IsAbs(file) {
   738  		return file
   739  	}
   740  	return filepath.Join(ts.cd, file)
   741  }
   742  
   743  // parse parses a single line as a list of space-separated arguments
   744  // subject to environment variable expansion (but not resplitting).
   745  // Single quotes around text disable splitting and expansion.
   746  // To embed a single quote, double it: 'Don''t communicate by sharing memory.'
   747  func (ts *testScript) parse(line string) []string {
   748  	ts.line = line
   749  
   750  	var (
   751  		args   []string
   752  		arg    string  // text of current arg so far (need to add line[start:i])
   753  		start  = -1    // if >= 0, position where current arg text chunk starts
   754  		quoted = false // currently processing quoted text
   755  	)
   756  	for i := 0; ; i++ {
   757  		if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
   758  			// Found arg-separating space.
   759  			if start >= 0 {
   760  				arg += ts.expand(line[start:i])
   761  				args = append(args, arg)
   762  				start = -1
   763  				arg = ""
   764  			}
   765  			if i >= len(line) || line[i] == '#' {
   766  				break
   767  			}
   768  			continue
   769  		}
   770  		if i >= len(line) {
   771  			ts.fatalf("unterminated quoted argument")
   772  		}
   773  		if line[i] == '\'' {
   774  			if !quoted {
   775  				// starting a quoted chunk
   776  				if start >= 0 {
   777  					arg += ts.expand(line[start:i])
   778  				}
   779  				start = i + 1
   780  				quoted = true
   781  				continue
   782  			}
   783  			// 'foo''bar' means foo'bar, like in rc shell and Pascal.
   784  			if i+1 < len(line) && line[i+1] == '\'' {
   785  				arg += line[start:i]
   786  				start = i + 1
   787  				i++ // skip over second ' before next iteration
   788  				continue
   789  			}
   790  			// ending a quoted chunk
   791  			arg += line[start:i]
   792  			start = i + 1
   793  			quoted = false
   794  			continue
   795  		}
   796  		// found character worth saving; make sure we're saving
   797  		if start < 0 {
   798  			start = i
   799  		}
   800  	}
   801  	return args
   802  }
   803  
   804  // diff returns a formatted diff of the two texts,
   805  // showing the entire text and the minimum line-level
   806  // additions and removals to turn text1 into text2.
   807  // (That is, lines only in text1 appear with a leading -,
   808  // and lines only in text2 appear with a leading +.)
   809  func diff(text1, text2 string) string {
   810  	if text1 != "" && !strings.HasSuffix(text1, "\n") {
   811  		text1 += "(missing final newline)"
   812  	}
   813  	lines1 := strings.Split(text1, "\n")
   814  	lines1 = lines1[:len(lines1)-1] // remove empty string after final line
   815  	if text2 != "" && !strings.HasSuffix(text2, "\n") {
   816  		text2 += "(missing final newline)"
   817  	}
   818  	lines2 := strings.Split(text2, "\n")
   819  	lines2 = lines2[:len(lines2)-1] // remove empty string after final line
   820  
   821  	// Naive dynamic programming algorithm for edit distance.
   822  	// https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm
   823  	// dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j]
   824  	// (The reversed indices make following the minimum cost path
   825  	// visit lines in the same order as in the text.)
   826  	dist := make([][]int, len(lines1)+1)
   827  	for i := range dist {
   828  		dist[i] = make([]int, len(lines2)+1)
   829  		if i == 0 {
   830  			for j := range dist[0] {
   831  				dist[0][j] = j
   832  			}
   833  			continue
   834  		}
   835  		for j := range dist[i] {
   836  			if j == 0 {
   837  				dist[i][0] = i
   838  				continue
   839  			}
   840  			cost := dist[i][j-1] + 1
   841  			if cost > dist[i-1][j]+1 {
   842  				cost = dist[i-1][j] + 1
   843  			}
   844  			if lines1[len(lines1)-i] == lines2[len(lines2)-j] {
   845  				if cost > dist[i-1][j-1] {
   846  					cost = dist[i-1][j-1]
   847  				}
   848  			}
   849  			dist[i][j] = cost
   850  		}
   851  	}
   852  
   853  	var buf strings.Builder
   854  	i, j := len(lines1), len(lines2)
   855  	for i > 0 || j > 0 {
   856  		cost := dist[i][j]
   857  		if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] {
   858  			fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i])
   859  			i--
   860  			j--
   861  		} else if i > 0 && cost == dist[i-1][j]+1 {
   862  			fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i])
   863  			i--
   864  		} else {
   865  			fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j])
   866  			j--
   867  		}
   868  	}
   869  	return buf.String()
   870  }
   871  
   872  var diffTests = []struct {
   873  	text1 string
   874  	text2 string
   875  	diff  string
   876  }{
   877  	{"a b c", "a b d e f", "a b -c +d +e +f"},
   878  	{"", "a b c", "+a +b +c"},
   879  	{"a b c", "", "-a -b -c"},
   880  	{"a b c", "d e f", "-a -b -c +d +e +f"},
   881  	{"a b c d e f", "a b d e f", "a b -c d e f"},
   882  	{"a b c e f", "a b c d e f", "a b c +d e f"},
   883  }
   884  
   885  func TestDiff(t *testing.T) {
   886  	for _, tt := range diffTests {
   887  		// Turn spaces into \n.
   888  		text1 := strings.ReplaceAll(tt.text1, " ", "\n")
   889  		if text1 != "" {
   890  			text1 += "\n"
   891  		}
   892  		text2 := strings.ReplaceAll(tt.text2, " ", "\n")
   893  		if text2 != "" {
   894  			text2 += "\n"
   895  		}
   896  		out := diff(text1, text2)
   897  		// Cut final \n, cut spaces, turn remaining \n into spaces.
   898  		out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ")
   899  		if out != tt.diff {
   900  			t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff)
   901  		}
   902  	}
   903  }