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 testscript
9
10import (
11	"bytes"
12	"context"
13	"flag"
14	"fmt"
15	"io/ioutil"
16	"os"
17	"os/exec"
18	"path/filepath"
19	"regexp"
20	"runtime"
21	"strings"
22	"sync/atomic"
23	"testing"
24	"time"
25
26	"github.com/rogpeppe/go-internal/imports"
27	"github.com/rogpeppe/go-internal/internal/os/execpath"
28	"github.com/rogpeppe/go-internal/par"
29	"github.com/rogpeppe/go-internal/testenv"
30	"github.com/rogpeppe/go-internal/txtar"
31)
32
33var execCache par.Cache
34
35// If -testwork is specified, the test prints the name of the temp directory
36// and does not remove it when done, so that a programmer can
37// poke at the test file tree afterward.
38var testWork = flag.Bool("testwork", false, "")
39
40// Env holds the environment to use at the start of a test script invocation.
41type Env struct {
42	// WorkDir holds the path to the root directory of the
43	// extracted files.
44	WorkDir string
45	// Vars holds the initial set environment variables that will be passed to the
46	// testscript commands.
47	Vars []string
48	// Cd holds the initial current working directory.
49	Cd string
50	// Values holds a map of arbitrary values for use by custom
51	// testscript commands. This enables Setup to pass arbitrary
52	// values (not just strings) through to custom commands.
53	Values map[interface{}]interface{}
54
55	ts *TestScript
56}
57
58// Value returns a value from Env.Values, or nil if no
59// value was set by Setup.
60func (ts *TestScript) Value(key interface{}) interface{} {
61	return ts.values[key]
62}
63
64// Defer arranges for f to be called at the end
65// of the test. If Defer is called multiple times, the
66// defers are executed in reverse order (similar
67// to Go's defer statement)
68func (e *Env) Defer(f func()) {
69	e.ts.Defer(f)
70}
71
72// Getenv retrieves the value of the environment variable named by the key. It
73// returns the value, which will be empty if the variable is not present.
74func (e *Env) Getenv(key string) string {
75	key = envvarname(key)
76	for i := len(e.Vars) - 1; i >= 0; i-- {
77		if pair := strings.SplitN(e.Vars[i], "=", 2); len(pair) == 2 && envvarname(pair[0]) == key {
78			return pair[1]
79		}
80	}
81	return ""
82}
83
84// Setenv sets the value of the environment variable named by the key. It
85// panics if key is invalid.
86func (e *Env) Setenv(key, value string) {
87	if key == "" || strings.IndexByte(key, '=') != -1 {
88		panic(fmt.Errorf("invalid environment variable key %q", key))
89	}
90	e.Vars = append(e.Vars, key+"="+value)
91}
92
93// T returns the t argument passed to the current test by the T.Run method.
94// Note that if the tests were started by calling Run,
95// the returned value will implement testing.TB.
96// Note that, despite that, the underlying value will not be of type
97// *testing.T because *testing.T does not implement T.
98//
99// If Cleanup is called on the returned value, the function will run
100// after any functions passed to Env.Defer.
101func (e *Env) T() T {
102	return e.ts.t
103}
104
105// Params holds parameters for a call to Run.
106type Params struct {
107	// Dir holds the name of the directory holding the scripts.
108	// All files in the directory with a .txt suffix will be considered
109	// as test scripts. By default the current directory is used.
110	// Dir is interpreted relative to the current test directory.
111	Dir string
112
113	// Setup is called, if not nil, to complete any setup required
114	// for a test. The WorkDir and Vars fields will have already
115	// been initialized and all the files extracted into WorkDir,
116	// and Cd will be the same as WorkDir.
117	// The Setup function may modify Vars and Cd as it wishes.
118	Setup func(*Env) error
119
120	// Condition is called, if not nil, to determine whether a particular
121	// condition is true. It's called only for conditions not in the
122	// standard set, and may be nil.
123	Condition func(cond string) (bool, error)
124
125	// Cmds holds a map of commands available to the script.
126	// It will only be consulted for commands not part of the standard set.
127	Cmds map[string]func(ts *TestScript, neg bool, args []string)
128
129	// TestWork specifies that working directories should be
130	// left intact for later inspection.
131	TestWork bool
132
133	// WorkdirRoot specifies the directory within which scripts' work
134	// directories will be created. Setting WorkdirRoot implies TestWork=true.
135	// If empty, the work directories will be created inside
136	// $GOTMPDIR/go-test-script*, where $GOTMPDIR defaults to os.TempDir().
137	WorkdirRoot string
138
139	// IgnoreMissedCoverage specifies that if coverage information
140	// is being generated (with the -test.coverprofile flag) and a subcommand
141	// function passed to RunMain fails to generate coverage information
142	// (for example because the function invoked os.Exit), then the
143	// error will be ignored.
144	IgnoreMissedCoverage bool
145
146	// UpdateScripts specifies that if a `cmp` command fails and its second
147	// argument refers to a file inside the testscript file, the command will
148	// succeed and the testscript file will be updated to reflect the actual
149	// content (which could be stdout, stderr or a real file).
150	//
151	// The content will be quoted with txtar.Quote if needed;
152	// a manual change will be needed if it is not unquoted in the
153	// script.
154	UpdateScripts bool
155}
156
157// RunDir runs the tests in the given directory. All files in dir with a ".txt"
158// are considered to be test files.
159func Run(t *testing.T, p Params) {
160	RunT(tshim{t}, p)
161}
162
163// T holds all the methods of the *testing.T type that
164// are used by testscript.
165type T interface {
166	Skip(...interface{})
167	Fatal(...interface{})
168	Parallel()
169	Log(...interface{})
170	FailNow()
171	Run(string, func(T))
172	// Verbose is usually implemented by the testing package
173	// directly rather than on the *testing.T type.
174	Verbose() bool
175}
176
177type tshim struct {
178	*testing.T
179}
180
181func (t tshim) Run(name string, f func(T)) {
182	t.T.Run(name, func(t *testing.T) {
183		f(tshim{t})
184	})
185}
186
187func (t tshim) Verbose() bool {
188	return testing.Verbose()
189}
190
191// RunT is like Run but uses an interface type instead of the concrete *testing.T
192// type to make it possible to use testscript functionality outside of go test.
193func RunT(t T, p Params) {
194	glob := filepath.Join(p.Dir, "*.txt")
195	files, err := filepath.Glob(glob)
196	if err != nil {
197		t.Fatal(err)
198	}
199	if len(files) == 0 {
200		t.Fatal(fmt.Sprintf("no scripts found matching glob: %v", glob))
201	}
202	testTempDir := p.WorkdirRoot
203	if testTempDir == "" {
204		testTempDir, err = ioutil.TempDir(os.Getenv("GOTMPDIR"), "go-test-script")
205		if err != nil {
206			t.Fatal(err)
207		}
208	} else {
209		p.TestWork = true
210	}
211	// The temp dir returned by ioutil.TempDir might be a sym linked dir (default
212	// behaviour in macOS). That could mess up matching that includes $WORK if,
213	// for example, an external program outputs resolved paths. Evaluating the
214	// dir here will ensure consistency.
215	testTempDir, err = filepath.EvalSymlinks(testTempDir)
216	if err != nil {
217		t.Fatal(err)
218	}
219	refCount := int32(len(files))
220	for _, file := range files {
221		file := file
222		name := strings.TrimSuffix(filepath.Base(file), ".txt")
223		t.Run(name, func(t T) {
224			t.Parallel()
225			ts := &TestScript{
226				t:             t,
227				testTempDir:   testTempDir,
228				name:          name,
229				file:          file,
230				params:        p,
231				ctxt:          context.Background(),
232				deferred:      func() {},
233				scriptFiles:   make(map[string]string),
234				scriptUpdates: make(map[string]string),
235			}
236			defer func() {
237				if p.TestWork || *testWork {
238					return
239				}
240				removeAll(ts.workdir)
241				if atomic.AddInt32(&refCount, -1) == 0 {
242					// This is the last subtest to finish. Remove the
243					// parent directory too.
244					os.Remove(testTempDir)
245				}
246			}()
247			ts.run()
248		})
249	}
250}
251
252// A TestScript holds execution state for a single test script.
253type TestScript struct {
254	params        Params
255	t             T
256	testTempDir   string
257	workdir       string                      // temporary work dir ($WORK)
258	log           bytes.Buffer                // test execution log (printed at end of test)
259	mark          int                         // offset of next log truncation
260	cd            string                      // current directory during test execution; initially $WORK/gopath/src
261	name          string                      // short name of test ("foo")
262	file          string                      // full file name ("testdata/script/foo.txt")
263	lineno        int                         // line number currently executing
264	line          string                      // line currently executing
265	env           []string                    // environment list (for os/exec)
266	envMap        map[string]string           // environment mapping (matches env; on Windows keys are lowercase)
267	values        map[interface{}]interface{} // values for custom commands
268	stdin         string                      // standard input to next 'go' command; set by 'stdin' command.
269	stdout        string                      // standard output from last 'go' command; for 'stdout' command
270	stderr        string                      // standard error from last 'go' command; for 'stderr' command
271	stopped       bool                        // test wants to stop early
272	start         time.Time                   // time phase started
273	background    []backgroundCmd             // backgrounded 'exec' and 'go' commands
274	deferred      func()                      // deferred cleanup actions.
275	archive       *txtar.Archive              // the testscript being run.
276	scriptFiles   map[string]string           // files stored in the txtar archive (absolute paths -> path in script)
277	scriptUpdates map[string]string           // updates to testscript files via UpdateScripts.
278
279	ctxt context.Context // per TestScript context
280}
281
282type backgroundCmd struct {
283	cmd  *exec.Cmd
284	wait <-chan struct{}
285	neg  bool // if true, cmd should fail
286}
287
288// setup sets up the test execution temporary directory and environment.
289// It returns the comment section of the txtar archive.
290func (ts *TestScript) setup() string {
291	ts.workdir = filepath.Join(ts.testTempDir, "script-"+ts.name)
292
293	// Establish a temporary directory in workdir, but use a prefix that ensures
294	// this directory will not be walked when resolving the ./... pattern from
295	// workdir. This is important because when resolving a ./... pattern, cmd/go
296	// (which is used by go/packages) creates temporary build files and
297	// directories. This can, and does, therefore interfere with the ./...
298	// pattern when used from workdir and can lead to race conditions within
299	// cmd/go as it walks directories to match the ./... pattern.
300	tmpDir := filepath.Join(ts.workdir, ".tmp")
301
302	ts.Check(os.MkdirAll(tmpDir, 0777))
303	env := &Env{
304		Vars: []string{
305			"WORK=" + ts.workdir, // must be first for ts.abbrev
306			"PATH=" + os.Getenv("PATH"),
307			homeEnvName() + "=/no-home",
308			tempEnvName() + "=" + tmpDir,
309			"devnull=" + os.DevNull,
310			"/=" + string(os.PathSeparator),
311			":=" + string(os.PathListSeparator),
312			"$=$",
313
314			// If we are collecting coverage profiles for merging into the main one,
315			// ensure the environment variable is forwarded to sub-processes.
316			"TESTSCRIPT_COVER_DIR=" + os.Getenv("TESTSCRIPT_COVER_DIR"),
317		},
318		WorkDir: ts.workdir,
319		Values:  make(map[interface{}]interface{}),
320		Cd:      ts.workdir,
321		ts:      ts,
322	}
323	// Must preserve SYSTEMROOT on Windows: https://github.com/golang/go/issues/25513 et al
324	if runtime.GOOS == "windows" {
325		env.Vars = append(env.Vars,
326			"SYSTEMROOT="+os.Getenv("SYSTEMROOT"),
327			"exe=.exe",
328		)
329	} else {
330		env.Vars = append(env.Vars,
331			"exe=",
332		)
333	}
334	ts.cd = env.Cd
335	// Unpack archive.
336	a, err := txtar.ParseFile(ts.file)
337	ts.Check(err)
338	ts.archive = a
339	for _, f := range a.Files {
340		name := ts.MkAbs(ts.expand(f.Name))
341		ts.scriptFiles[name] = f.Name
342		ts.Check(os.MkdirAll(filepath.Dir(name), 0777))
343		ts.Check(ioutil.WriteFile(name, f.Data, 0666))
344	}
345	// Run any user-defined setup.
346	if ts.params.Setup != nil {
347		ts.Check(ts.params.Setup(env))
348	}
349	ts.cd = env.Cd
350	ts.env = env.Vars
351	ts.values = env.Values
352
353	ts.envMap = make(map[string]string)
354	for _, kv := range ts.env {
355		if i := strings.Index(kv, "="); i >= 0 {
356			ts.envMap[envvarname(kv[:i])] = kv[i+1:]
357		}
358	}
359	return string(a.Comment)
360}
361
362// run runs the test script.
363func (ts *TestScript) run() {
364	// Truncate log at end of last phase marker,
365	// discarding details of successful phase.
366	rewind := func() {
367		if !ts.t.Verbose() {
368			ts.log.Truncate(ts.mark)
369		}
370	}
371
372	// Insert elapsed time for phase at end of phase marker
373	markTime := func() {
374		if ts.mark > 0 && !ts.start.IsZero() {
375			afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
376			ts.log.Truncate(ts.mark - 1) // cut \n and afterMark
377			fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds())
378			ts.log.Write(afterMark)
379		}
380		ts.start = time.Time{}
381	}
382
383	defer func() {
384		// On a normal exit from the test loop, background processes are cleaned up
385		// before we print PASS. If we return early (e.g., due to a test failure),
386		// don't print anything about the processes that were still running.
387		for _, bg := range ts.background {
388			interruptProcess(bg.cmd.Process)
389		}
390		for _, bg := range ts.background {
391			<-bg.wait
392		}
393		ts.background = nil
394
395		markTime()
396		// Flush testScript log to testing.T log.
397		ts.t.Log("\n" + ts.abbrev(ts.log.String()))
398	}()
399	defer func() {
400		ts.deferred()
401	}()
402	script := ts.setup()
403
404	// With -v or -testwork, start log with full environment.
405	if *testWork || ts.t.Verbose() {
406		// Display environment.
407		ts.cmdEnv(false, nil)
408		fmt.Fprintf(&ts.log, "\n")
409		ts.mark = ts.log.Len()
410	}
411	defer ts.applyScriptUpdates()
412
413	// Run script.
414	// See testdata/script/README for documentation of script form.
415Script:
416	for script != "" {
417		// Extract next line.
418		ts.lineno++
419		var line string
420		if i := strings.Index(script, "\n"); i >= 0 {
421			line, script = script[:i], script[i+1:]
422		} else {
423			line, script = script, ""
424		}
425
426		// # is a comment indicating the start of new phase.
427		if strings.HasPrefix(line, "#") {
428			// If there was a previous phase, it succeeded,
429			// so rewind the log to delete its details (unless -v is in use).
430			// If nothing has happened at all since the mark,
431			// rewinding is a no-op and adding elapsed time
432			// for doing nothing is meaningless, so don't.
433			if ts.log.Len() > ts.mark {
434				rewind()
435				markTime()
436			}
437			// Print phase heading and mark start of phase output.
438			fmt.Fprintf(&ts.log, "%s\n", line)
439			ts.mark = ts.log.Len()
440			ts.start = time.Now()
441			continue
442		}
443
444		// Parse input line. Ignore blanks entirely.
445		args := ts.parse(line)
446		if len(args) == 0 {
447			continue
448		}
449
450		// Echo command to log.
451		fmt.Fprintf(&ts.log, "> %s\n", line)
452
453		// Command prefix [cond] means only run this command if cond is satisfied.
454		for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
455			cond := args[0]
456			cond = cond[1 : len(cond)-1]
457			cond = strings.TrimSpace(cond)
458			args = args[1:]
459			if len(args) == 0 {
460				ts.Fatalf("missing command after condition")
461			}
462			want := true
463			if strings.HasPrefix(cond, "!") {
464				want = false
465				cond = strings.TrimSpace(cond[1:])
466			}
467			ok, err := ts.condition(cond)
468			if err != nil {
469				ts.Fatalf("bad condition %q: %v", cond, err)
470			}
471			if ok != want {
472				// Don't run rest of line.
473				continue Script
474			}
475		}
476
477		// Command prefix ! means negate the expectations about this command:
478		// go command should fail, match should not be found, etc.
479		neg := false
480		if args[0] == "!" {
481			neg = true
482			args = args[1:]
483			if len(args) == 0 {
484				ts.Fatalf("! on line by itself")
485			}
486		}
487
488		// Run command.
489		cmd := scriptCmds[args[0]]
490		if cmd == nil {
491			cmd = ts.params.Cmds[args[0]]
492		}
493		if cmd == nil {
494			ts.Fatalf("unknown command %q", args[0])
495		}
496		cmd(ts, neg, args[1:])
497
498		// Command can ask script to stop early.
499		if ts.stopped {
500			// Break instead of returning, so that we check the status of any
501			// background processes and print PASS.
502			break
503		}
504	}
505
506	for _, bg := range ts.background {
507		interruptProcess(bg.cmd.Process)
508	}
509	ts.cmdWait(false, nil)
510
511	// Final phase ended.
512	rewind()
513	markTime()
514	if !ts.stopped {
515		fmt.Fprintf(&ts.log, "PASS\n")
516	}
517}
518
519func (ts *TestScript) applyScriptUpdates() {
520	if len(ts.scriptUpdates) == 0 {
521		return
522	}
523	for name, content := range ts.scriptUpdates {
524		found := false
525		for i := range ts.archive.Files {
526			f := &ts.archive.Files[i]
527			if f.Name != name {
528				continue
529			}
530			data := []byte(content)
531			if txtar.NeedsQuote(data) {
532				data1, err := txtar.Quote(data)
533				if err != nil {
534					ts.t.Fatal(fmt.Sprintf("cannot update script file %q: %v", f.Name, err))
535					continue
536				}
537				data = data1
538			}
539			f.Data = data
540			found = true
541		}
542		// Sanity check.
543		if !found {
544			panic("script update file not found")
545		}
546	}
547	if err := ioutil.WriteFile(ts.file, txtar.Format(ts.archive), 0666); err != nil {
548		ts.t.Fatal("cannot update script: ", err)
549	}
550	ts.Logf("%s updated", ts.file)
551}
552
553// condition reports whether the given condition is satisfied.
554func (ts *TestScript) condition(cond string) (bool, error) {
555	switch cond {
556	case "short":
557		return testing.Short(), nil
558	case "net":
559		return testenv.HasExternalNetwork(), nil
560	case "link":
561		return testenv.HasLink(), nil
562	case "symlink":
563		return testenv.HasSymlink(), nil
564	case runtime.GOOS, runtime.GOARCH:
565		return true, nil
566	default:
567		if imports.KnownArch[cond] || imports.KnownOS[cond] {
568			return false, nil
569		}
570		if strings.HasPrefix(cond, "exec:") {
571			prog := cond[len("exec:"):]
572			ok := execCache.Do(prog, func() interface{} {
573				_, err := execpath.Look(prog, ts.Getenv)
574				return err == nil
575			}).(bool)
576			return ok, nil
577		}
578		if ts.params.Condition != nil {
579			return ts.params.Condition(cond)
580		}
581		ts.Fatalf("unknown condition %q", cond)
582		panic("unreachable")
583	}
584}
585
586// Helpers for command implementations.
587
588// abbrev abbreviates the actual work directory in the string s to the literal string "$WORK".
589func (ts *TestScript) abbrev(s string) string {
590	s = strings.Replace(s, ts.workdir, "$WORK", -1)
591	if *testWork || ts.params.TestWork {
592		// Expose actual $WORK value in environment dump on first line of work script,
593		// so that the user can find out what directory -testwork left behind.
594		s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n")
595	}
596	return s
597}
598
599// Defer arranges for f to be called at the end
600// of the test. If Defer is called multiple times, the
601// defers are executed in reverse order (similar
602// to Go's defer statement)
603func (ts *TestScript) Defer(f func()) {
604	old := ts.deferred
605	ts.deferred = func() {
606		defer old()
607		f()
608	}
609}
610
611// Check calls ts.Fatalf if err != nil.
612func (ts *TestScript) Check(err error) {
613	if err != nil {
614		ts.Fatalf("%v", err)
615	}
616}
617
618// Logf appends the given formatted message to the test log transcript.
619func (ts *TestScript) Logf(format string, args ...interface{}) {
620	format = strings.TrimSuffix(format, "\n")
621	fmt.Fprintf(&ts.log, format, args...)
622	ts.log.WriteByte('\n')
623}
624
625// exec runs the given command line (an actual subprocess, not simulated)
626// in ts.cd with environment ts.env and then returns collected standard output and standard error.
627func (ts *TestScript) exec(command string, args ...string) (stdout, stderr string, err error) {
628	cmd, err := ts.buildExecCmd(command, args...)
629	if err != nil {
630		return "", "", err
631	}
632	cmd.Dir = ts.cd
633	cmd.Env = append(ts.env, "PWD="+ts.cd)
634	cmd.Stdin = strings.NewReader(ts.stdin)
635	var stdoutBuf, stderrBuf strings.Builder
636	cmd.Stdout = &stdoutBuf
637	cmd.Stderr = &stderrBuf
638	if err = cmd.Start(); err == nil {
639		err = ctxWait(ts.ctxt, cmd)
640	}
641	ts.stdin = ""
642	return stdoutBuf.String(), stderrBuf.String(), err
643}
644
645// execBackground starts the given command line (an actual subprocess, not simulated)
646// in ts.cd with environment ts.env.
647func (ts *TestScript) execBackground(command string, args ...string) (*exec.Cmd, error) {
648	cmd, err := ts.buildExecCmd(command, args...)
649	if err != nil {
650		return nil, err
651	}
652	cmd.Dir = ts.cd
653	cmd.Env = append(ts.env, "PWD="+ts.cd)
654	var stdoutBuf, stderrBuf strings.Builder
655	cmd.Stdin = strings.NewReader(ts.stdin)
656	cmd.Stdout = &stdoutBuf
657	cmd.Stderr = &stderrBuf
658	ts.stdin = ""
659	return cmd, cmd.Start()
660}
661
662func (ts *TestScript) buildExecCmd(command string, args ...string) (*exec.Cmd, error) {
663	if filepath.Base(command) == command {
664		if lp, err := execpath.Look(command, ts.Getenv); err != nil {
665			return nil, err
666		} else {
667			command = lp
668		}
669	}
670	return exec.Command(command, args...), nil
671}
672
673// BackgroundCmds returns a slice containing all the commands that have
674// been started in the background since the most recent wait command, or
675// the start of the script if wait has not been called.
676func (ts *TestScript) BackgroundCmds() []*exec.Cmd {
677	cmds := make([]*exec.Cmd, len(ts.background))
678	for i, b := range ts.background {
679		cmds[i] = b.cmd
680	}
681	return cmds
682}
683
684// ctxWait is like cmd.Wait, but terminates cmd with os.Interrupt if ctx becomes done.
685//
686// This differs from exec.CommandContext in that it prefers os.Interrupt over os.Kill.
687// (See https://golang.org/issue/21135.)
688func ctxWait(ctx context.Context, cmd *exec.Cmd) error {
689	errc := make(chan error, 1)
690	go func() { errc <- cmd.Wait() }()
691
692	select {
693	case err := <-errc:
694		return err
695	case <-ctx.Done():
696		interruptProcess(cmd.Process)
697		return <-errc
698	}
699}
700
701// interruptProcess sends os.Interrupt to p if supported, or os.Kill otherwise.
702func interruptProcess(p *os.Process) {
703	if err := p.Signal(os.Interrupt); err != nil {
704		// Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on
705		// Windows; using it with os.Process.Signal will return an error.”
706		// Fall back to Kill instead.
707		p.Kill()
708	}
709}
710
711// Exec runs the given command and saves its stdout and stderr so
712// they can be inspected by subsequent script commands.
713func (ts *TestScript) Exec(command string, args ...string) error {
714	var err error
715	ts.stdout, ts.stderr, err = ts.exec(command, args...)
716	if ts.stdout != "" {
717		ts.Logf("[stdout]\n%s", ts.stdout)
718	}
719	if ts.stderr != "" {
720		ts.Logf("[stderr]\n%s", ts.stderr)
721	}
722	return err
723}
724
725// expand applies environment variable expansion to the string s.
726func (ts *TestScript) expand(s string) string {
727	return os.Expand(s, func(key string) string {
728		if key1 := strings.TrimSuffix(key, "@R"); len(key1) != len(key) {
729			return regexp.QuoteMeta(ts.Getenv(key1))
730		}
731		return ts.Getenv(key)
732	})
733}
734
735// fatalf aborts the test with the given failure message.
736func (ts *TestScript) Fatalf(format string, args ...interface{}) {
737	fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
738	ts.t.FailNow()
739}
740
741// MkAbs interprets file relative to the test script's current directory
742// and returns the corresponding absolute path.
743func (ts *TestScript) MkAbs(file string) string {
744	if filepath.IsAbs(file) {
745		return file
746	}
747	return filepath.Join(ts.cd, file)
748}
749
750// ReadFile returns the contents of the file with the
751// given name, intepreted relative to the test script's
752// current directory. It interprets "stdout" and "stderr" to
753// mean the standard output or standard error from
754// the most recent exec or wait command respectively.
755//
756// If the file cannot be read, the script fails.
757func (ts *TestScript) ReadFile(file string) string {
758	switch file {
759	case "stdout":
760		return ts.stdout
761	case "stderr":
762		return ts.stderr
763	default:
764		file = ts.MkAbs(file)
765		data, err := ioutil.ReadFile(file)
766		ts.Check(err)
767		return string(data)
768	}
769}
770
771// Setenv sets the value of the environment variable named by the key.
772func (ts *TestScript) Setenv(key, value string) {
773	ts.env = append(ts.env, key+"="+value)
774	ts.envMap[envvarname(key)] = value
775}
776
777// Getenv gets the value of the environment variable named by the key.
778func (ts *TestScript) Getenv(key string) string {
779	return ts.envMap[envvarname(key)]
780}
781
782// parse parses a single line as a list of space-separated arguments
783// subject to environment variable expansion (but not resplitting).
784// Single quotes around text disable splitting and expansion.
785// To embed a single quote, double it: 'Don''t communicate by sharing memory.'
786func (ts *TestScript) parse(line string) []string {
787	ts.line = line
788
789	var (
790		args   []string
791		arg    string  // text of current arg so far (need to add line[start:i])
792		start  = -1    // if >= 0, position where current arg text chunk starts
793		quoted = false // currently processing quoted text
794	)
795	for i := 0; ; i++ {
796		if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
797			// Found arg-separating space.
798			if start >= 0 {
799				arg += ts.expand(line[start:i])
800				args = append(args, arg)
801				start = -1
802				arg = ""
803			}
804			if i >= len(line) || line[i] == '#' {
805				break
806			}
807			continue
808		}
809		if i >= len(line) {
810			ts.Fatalf("unterminated quoted argument")
811		}
812		if line[i] == '\'' {
813			if !quoted {
814				// starting a quoted chunk
815				if start >= 0 {
816					arg += ts.expand(line[start:i])
817				}
818				start = i + 1
819				quoted = true
820				continue
821			}
822			// 'foo''bar' means foo'bar, like in rc shell and Pascal.
823			if i+1 < len(line) && line[i+1] == '\'' {
824				arg += line[start:i]
825				start = i + 1
826				i++ // skip over second ' before next iteration
827				continue
828			}
829			// ending a quoted chunk
830			arg += line[start:i]
831			start = i + 1
832			quoted = false
833			continue
834		}
835		// found character worth saving; make sure we're saving
836		if start < 0 {
837			start = i
838		}
839	}
840	return args
841}
842
843func removeAll(dir string) error {
844	// module cache has 0444 directories;
845	// make them writable in order to remove content.
846	filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
847		if err != nil {
848			return nil // ignore errors walking in file system
849		}
850		if info.IsDir() {
851			os.Chmod(path, 0777)
852		}
853		return nil
854	})
855	return os.RemoveAll(dir)
856}
857
858func homeEnvName() string {
859	switch runtime.GOOS {
860	case "windows":
861		return "USERPROFILE"
862	case "plan9":
863		return "home"
864	default:
865		return "HOME"
866	}
867}
868
869func tempEnvName() string {
870	switch runtime.GOOS {
871	case "windows":
872		return "TMP"
873	case "plan9":
874		return "TMPDIR" // actually plan 9 doesn't have one at all but this is fine
875	default:
876		return "TMPDIR"
877	}
878}
879