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
8package main_test
9
10import (
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.
39func 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.
99type 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
121type backgroundCmd struct {
122	want           simpleStatus
123	args           []string
124	done           <-chan struct{}
125	err            error
126	stdout, stderr strings.Builder
127}
128
129type simpleStatus string
130
131const (
132	success          simpleStatus = ""
133	failure          simpleStatus = "!"
134	successOrFailure simpleStatus = "?"
135)
136
137var 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.
148func (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.
208func 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
217var execCache par.Cache
218
219// run runs the test script.
220func (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)
275Script:
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
413var (
414	onceCaseSensitive sync.Once
415	caseSensitive     bool
416)
417
418func 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//
453var 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.
479var 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.
486func (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.
500func (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.
512func (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.
538func (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.
560func (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.
577func (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
592func (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.
625func (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.
677func (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.
717func (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.
754func (ts *testScript) cmdExists(want simpleStatus, args []string) {
755	if want == successOrFailure {
756		ts.fatalf("unsupported: %v exists", want)
757	}
758	var readonly, exec bool
759loop:
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.
799func (ts *testScript) cmdGo(want simpleStatus, args []string) {
800	ts.cmdExec(want, append([]string{testGo}, args...))
801}
802
803// mkdir creates directories.
804func (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.
817func (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.
832func (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.
852func (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.
877func (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.
882func (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.
888func (ts *testScript) cmdGrep(want simpleStatus, args []string) {
889	scriptMatch(ts, want, args, "", "grep")
890}
891
892// scriptMatch implements both stdout and stderr.
893func 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).
975func (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.
991func (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.
1004func (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".
1042func (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.
1053func (ts *testScript) check(err error) {
1054	if err != nil {
1055		ts.fatalf("%v", err)
1056	}
1057}
1058
1059func (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.
1093func (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.
1104func (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.
1135func 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.
1190func (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.
1207func (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.
1214func (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.
1222type condition struct {
1223	want bool
1224	tag  string
1225}
1226
1227// A command is a complete command parsed from a script.
1228type 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.'
1239func (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 +.)
1349func 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
1412var 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
1425func 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