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
5package testscript
6
7import (
8	"bufio"
9	"bytes"
10	"fmt"
11	"io/ioutil"
12	"os"
13	"os/exec"
14	"path/filepath"
15	"regexp"
16	"strconv"
17	"strings"
18
19	"github.com/pkg/diff"
20	"github.com/rogpeppe/go-internal/txtar"
21)
22
23// scriptCmds are the script command implementations.
24// Keep list and the implementations below sorted by name.
25//
26// NOTE: If you make changes here, update doc.go.
27//
28var scriptCmds = map[string]func(*TestScript, bool, []string){
29	"cd":       (*TestScript).cmdCd,
30	"chmod":    (*TestScript).cmdChmod,
31	"cmp":      (*TestScript).cmdCmp,
32	"cmpenv":   (*TestScript).cmdCmpenv,
33	"cp":       (*TestScript).cmdCp,
34	"env":      (*TestScript).cmdEnv,
35	"exec":     (*TestScript).cmdExec,
36	"exists":   (*TestScript).cmdExists,
37	"grep":     (*TestScript).cmdGrep,
38	"mkdir":    (*TestScript).cmdMkdir,
39	"rm":       (*TestScript).cmdRm,
40	"unquote":  (*TestScript).cmdUnquote,
41	"skip":     (*TestScript).cmdSkip,
42	"stdin":    (*TestScript).cmdStdin,
43	"stderr":   (*TestScript).cmdStderr,
44	"stdout":   (*TestScript).cmdStdout,
45	"stop":     (*TestScript).cmdStop,
46	"symlink":  (*TestScript).cmdSymlink,
47	"unix2dos": (*TestScript).cmdUNIX2DOS,
48	"wait":     (*TestScript).cmdWait,
49}
50
51// cd changes to a different directory.
52func (ts *TestScript) cmdCd(neg bool, args []string) {
53	if neg {
54		ts.Fatalf("unsupported: ! cd")
55	}
56	if len(args) != 1 {
57		ts.Fatalf("usage: cd dir")
58	}
59
60	dir := args[0]
61	if !filepath.IsAbs(dir) {
62		dir = filepath.Join(ts.cd, dir)
63	}
64	info, err := os.Stat(dir)
65	if os.IsNotExist(err) {
66		ts.Fatalf("directory %s does not exist", dir)
67	}
68	ts.Check(err)
69	if !info.IsDir() {
70		ts.Fatalf("%s is not a directory", dir)
71	}
72	ts.cd = dir
73	ts.Logf("%s\n", ts.cd)
74}
75
76func (ts *TestScript) cmdChmod(neg bool, args []string) {
77	if neg {
78		ts.Fatalf("unsupported: ! chmod")
79	}
80	if len(args) != 2 {
81		ts.Fatalf("usage: chmod perm paths...")
82	}
83	perm, err := strconv.ParseUint(args[0], 8, 32)
84	if err != nil || perm&uint64(os.ModePerm) != perm {
85		ts.Fatalf("invalid mode: %s", args[0])
86	}
87	for _, arg := range args[1:] {
88		path := arg
89		if !filepath.IsAbs(path) {
90			path = filepath.Join(ts.cd, arg)
91		}
92		err := os.Chmod(path, os.FileMode(perm))
93		ts.Check(err)
94	}
95}
96
97// cmp compares two files.
98func (ts *TestScript) cmdCmp(neg bool, args []string) {
99	if neg {
100		// It would be strange to say "this file can have any content except this precise byte sequence".
101		ts.Fatalf("unsupported: ! cmp")
102	}
103	if len(args) != 2 {
104		ts.Fatalf("usage: cmp file1 file2")
105	}
106
107	ts.doCmdCmp(args, false)
108}
109
110// cmpenv compares two files with environment variable substitution.
111func (ts *TestScript) cmdCmpenv(neg bool, args []string) {
112	if neg {
113		ts.Fatalf("unsupported: ! cmpenv")
114	}
115	if len(args) != 2 {
116		ts.Fatalf("usage: cmpenv file1 file2")
117	}
118	ts.doCmdCmp(args, true)
119}
120
121func (ts *TestScript) doCmdCmp(args []string, env bool) {
122	name1, name2 := args[0], args[1]
123	text1 := ts.ReadFile(name1)
124
125	absName2 := ts.MkAbs(name2)
126	data, err := ioutil.ReadFile(absName2)
127	ts.Check(err)
128	text2 := string(data)
129	if env {
130		text2 = ts.expand(text2)
131	}
132	if text1 == text2 {
133		return
134	}
135	if ts.params.UpdateScripts && !env {
136		if scriptFile, ok := ts.scriptFiles[absName2]; ok {
137			ts.scriptUpdates[scriptFile] = text1
138			return
139		}
140		// The file being compared against isn't in the txtar archive, so don't
141		// update the script.
142	}
143
144	var sb strings.Builder
145	if err := diff.Text(name1, name2, text1, text2, &sb); err != nil {
146		ts.Check(err)
147	}
148
149	ts.Logf("%s", sb.String())
150	ts.Fatalf("%s and %s differ", name1, name2)
151}
152
153// cp copies files, maybe eventually directories.
154func (ts *TestScript) cmdCp(neg bool, args []string) {
155	if neg {
156		ts.Fatalf("unsupported: ! cp")
157	}
158	if len(args) < 2 {
159		ts.Fatalf("usage: cp src... dst")
160	}
161
162	dst := ts.MkAbs(args[len(args)-1])
163	info, err := os.Stat(dst)
164	dstDir := err == nil && info.IsDir()
165	if len(args) > 2 && !dstDir {
166		ts.Fatalf("cp: destination %s is not a directory", dst)
167	}
168
169	for _, arg := range args[:len(args)-1] {
170		var (
171			src  string
172			data []byte
173			mode os.FileMode
174		)
175		switch arg {
176		case "stdout":
177			src = arg
178			data = []byte(ts.stdout)
179			mode = 0666
180		case "stderr":
181			src = arg
182			data = []byte(ts.stderr)
183			mode = 0666
184		default:
185			src = ts.MkAbs(arg)
186			info, err := os.Stat(src)
187			ts.Check(err)
188			mode = info.Mode() & 0777
189			data, err = ioutil.ReadFile(src)
190			ts.Check(err)
191		}
192		targ := dst
193		if dstDir {
194			targ = filepath.Join(dst, filepath.Base(src))
195		}
196		ts.Check(ioutil.WriteFile(targ, data, mode))
197	}
198}
199
200// env displays or adds to the environment.
201func (ts *TestScript) cmdEnv(neg bool, args []string) {
202	if neg {
203		ts.Fatalf("unsupported: ! env")
204	}
205	if len(args) == 0 {
206		printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once
207		for _, kv := range ts.env {
208			k := envvarname(kv[:strings.Index(kv, "=")])
209			if !printed[k] {
210				printed[k] = true
211				ts.Logf("%s=%s\n", k, ts.envMap[k])
212			}
213		}
214		return
215	}
216	for _, env := range args {
217		i := strings.Index(env, "=")
218		if i < 0 {
219			// Display value instead of setting it.
220			ts.Logf("%s=%s\n", env, ts.Getenv(env))
221			continue
222		}
223		ts.Setenv(env[:i], env[i+1:])
224	}
225}
226
227// exec runs the given command.
228func (ts *TestScript) cmdExec(neg bool, args []string) {
229	if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
230		ts.Fatalf("usage: exec program [args...] [&]")
231	}
232
233	var err error
234	if len(args) > 0 && args[len(args)-1] == "&" {
235		var cmd *exec.Cmd
236		cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...)
237		if err == nil {
238			wait := make(chan struct{})
239			go func() {
240				ctxWait(ts.ctxt, cmd)
241				close(wait)
242			}()
243			ts.background = append(ts.background, backgroundCmd{cmd, wait, neg})
244		}
245		ts.stdout, ts.stderr = "", ""
246	} else {
247		ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
248		if ts.stdout != "" {
249			fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
250		}
251		if ts.stderr != "" {
252			fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
253		}
254		if err == nil && neg {
255			ts.Fatalf("unexpected command success")
256		}
257	}
258
259	if err != nil {
260		fmt.Fprintf(&ts.log, "[%v]\n", err)
261		if ts.ctxt.Err() != nil {
262			ts.Fatalf("test timed out while running command")
263		} else if !neg {
264			ts.Fatalf("unexpected command failure")
265		}
266	}
267}
268
269// exists checks that the list of files exists.
270func (ts *TestScript) cmdExists(neg bool, args []string) {
271	var readonly bool
272	if len(args) > 0 && args[0] == "-readonly" {
273		readonly = true
274		args = args[1:]
275	}
276	if len(args) == 0 {
277		ts.Fatalf("usage: exists [-readonly] file...")
278	}
279
280	for _, file := range args {
281		file = ts.MkAbs(file)
282		info, err := os.Stat(file)
283		if err == nil && neg {
284			what := "file"
285			if info.IsDir() {
286				what = "directory"
287			}
288			ts.Fatalf("%s %s unexpectedly exists", what, file)
289		}
290		if err != nil && !neg {
291			ts.Fatalf("%s does not exist", file)
292		}
293		if err == nil && !neg && readonly && info.Mode()&0222 != 0 {
294			ts.Fatalf("%s exists but is writable", file)
295		}
296	}
297}
298
299// mkdir creates directories.
300func (ts *TestScript) cmdMkdir(neg bool, args []string) {
301	if neg {
302		ts.Fatalf("unsupported: ! mkdir")
303	}
304	if len(args) < 1 {
305		ts.Fatalf("usage: mkdir dir...")
306	}
307	for _, arg := range args {
308		ts.Check(os.MkdirAll(ts.MkAbs(arg), 0777))
309	}
310}
311
312// unquote unquotes files.
313func (ts *TestScript) cmdUnquote(neg bool, args []string) {
314	if neg {
315		ts.Fatalf("unsupported: ! unquote")
316	}
317	for _, arg := range args {
318		file := ts.MkAbs(arg)
319		data, err := ioutil.ReadFile(file)
320		ts.Check(err)
321		data, err = txtar.Unquote(data)
322		ts.Check(err)
323		err = ioutil.WriteFile(file, data, 0666)
324		ts.Check(err)
325	}
326}
327
328// rm removes files or directories.
329func (ts *TestScript) cmdRm(neg bool, args []string) {
330	if neg {
331		ts.Fatalf("unsupported: ! rm")
332	}
333	if len(args) < 1 {
334		ts.Fatalf("usage: rm file...")
335	}
336	for _, arg := range args {
337		file := ts.MkAbs(arg)
338		removeAll(file)              // does chmod and then attempts rm
339		ts.Check(os.RemoveAll(file)) // report error
340	}
341}
342
343// skip marks the test skipped.
344func (ts *TestScript) cmdSkip(neg bool, args []string) {
345	if len(args) > 1 {
346		ts.Fatalf("usage: skip [msg]")
347	}
348	if neg {
349		ts.Fatalf("unsupported: ! skip")
350	}
351
352	// Before we mark the test as skipped, shut down any background processes and
353	// make sure they have returned the correct status.
354	for _, bg := range ts.background {
355		interruptProcess(bg.cmd.Process)
356	}
357	ts.cmdWait(false, nil)
358
359	if len(args) == 1 {
360		ts.t.Skip(args[0])
361	}
362	ts.t.Skip()
363}
364
365func (ts *TestScript) cmdStdin(neg bool, args []string) {
366	if neg {
367		ts.Fatalf("unsupported: ! stdin")
368	}
369	if len(args) != 1 {
370		ts.Fatalf("usage: stdin filename")
371	}
372	ts.stdin = ts.ReadFile(args[0])
373}
374
375// stdout checks that the last go command standard output matches a regexp.
376func (ts *TestScript) cmdStdout(neg bool, args []string) {
377	scriptMatch(ts, neg, args, ts.stdout, "stdout")
378}
379
380// stderr checks that the last go command standard output matches a regexp.
381func (ts *TestScript) cmdStderr(neg bool, args []string) {
382	scriptMatch(ts, neg, args, ts.stderr, "stderr")
383}
384
385// grep checks that file content matches a regexp.
386// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax.
387func (ts *TestScript) cmdGrep(neg bool, args []string) {
388	scriptMatch(ts, neg, args, "", "grep")
389}
390
391// stop stops execution of the test (marking it passed).
392func (ts *TestScript) cmdStop(neg bool, args []string) {
393	if neg {
394		ts.Fatalf("unsupported: ! stop")
395	}
396	if len(args) > 1 {
397		ts.Fatalf("usage: stop [msg]")
398	}
399	if len(args) == 1 {
400		ts.Logf("stop: %s\n", args[0])
401	} else {
402		ts.Logf("stop\n")
403	}
404	ts.stopped = true
405}
406
407// symlink creates a symbolic link.
408func (ts *TestScript) cmdSymlink(neg bool, args []string) {
409	if neg {
410		ts.Fatalf("unsupported: ! symlink")
411	}
412	if len(args) != 3 || args[1] != "->" {
413		ts.Fatalf("usage: symlink file -> target")
414	}
415	// Note that the link target args[2] is not interpreted with MkAbs:
416	// it will be interpreted relative to the directory file is in.
417	ts.Check(os.Symlink(args[2], ts.MkAbs(args[0])))
418}
419
420// cmdUNIX2DOS converts files from UNIX line endings to DOS line endings.
421func (ts *TestScript) cmdUNIX2DOS(neg bool, args []string) {
422	if neg {
423		ts.Fatalf("unsupported: ! unix2dos")
424	}
425	if len(args) < 1 {
426		ts.Fatalf("usage: unix2dos paths...")
427	}
428	for _, arg := range args {
429		filename := ts.MkAbs(arg)
430		data, err := ioutil.ReadFile(filename)
431		ts.Check(err)
432		dosData, err := unix2DOS(data)
433		ts.Check(err)
434		if err := ioutil.WriteFile(filename, dosData, 0666); err != nil {
435			ts.Fatalf("%s: %v", filename, err)
436		}
437	}
438}
439
440// Tait waits for background commands to exit, setting stderr and stdout to their result.
441func (ts *TestScript) cmdWait(neg bool, args []string) {
442	if neg {
443		ts.Fatalf("unsupported: ! wait")
444	}
445	if len(args) > 0 {
446		ts.Fatalf("usage: wait")
447	}
448
449	var stdouts, stderrs []string
450	for _, bg := range ts.background {
451		<-bg.wait
452
453		args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...)
454		fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState)
455
456		cmdStdout := bg.cmd.Stdout.(*strings.Builder).String()
457		if cmdStdout != "" {
458			fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
459			stdouts = append(stdouts, cmdStdout)
460		}
461
462		cmdStderr := bg.cmd.Stderr.(*strings.Builder).String()
463		if cmdStderr != "" {
464			fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
465			stderrs = append(stderrs, cmdStderr)
466		}
467
468		if bg.cmd.ProcessState.Success() {
469			if bg.neg {
470				ts.Fatalf("unexpected command success")
471			}
472		} else {
473			if ts.ctxt.Err() != nil {
474				ts.Fatalf("test timed out while running command")
475			} else if !bg.neg {
476				ts.Fatalf("unexpected command failure")
477			}
478		}
479	}
480
481	ts.stdout = strings.Join(stdouts, "")
482	ts.stderr = strings.Join(stderrs, "")
483	ts.background = nil
484}
485
486// scriptMatch implements both stdout and stderr.
487func scriptMatch(ts *TestScript, neg bool, args []string, text, name string) {
488	n := 0
489	if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
490		if neg {
491			ts.Fatalf("cannot use -count= with negated match")
492		}
493		var err error
494		n, err = strconv.Atoi(args[0][len("-count="):])
495		if err != nil {
496			ts.Fatalf("bad -count=: %v", err)
497		}
498		if n < 1 {
499			ts.Fatalf("bad -count=: must be at least 1")
500		}
501		args = args[1:]
502	}
503
504	extraUsage := ""
505	want := 1
506	if name == "grep" {
507		extraUsage = " file"
508		want = 2
509	}
510	if len(args) != want {
511		ts.Fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
512	}
513
514	pattern := args[0]
515	re, err := regexp.Compile(`(?m)` + pattern)
516	ts.Check(err)
517
518	isGrep := name == "grep"
519	if isGrep {
520		name = args[1] // for error messages
521		data, err := ioutil.ReadFile(ts.MkAbs(args[1]))
522		ts.Check(err)
523		text = string(data)
524	}
525
526	if neg {
527		if re.MatchString(text) {
528			if isGrep {
529				ts.Logf("[%s]\n%s\n", name, text)
530			}
531			ts.Fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
532		}
533	} else {
534		if !re.MatchString(text) {
535			if isGrep {
536				ts.Logf("[%s]\n%s\n", name, text)
537			}
538			ts.Fatalf("no match for %#q found in %s", pattern, name)
539		}
540		if n > 0 {
541			count := len(re.FindAllString(text, -1))
542			if count != n {
543				if isGrep {
544					ts.Logf("[%s]\n%s\n", name, text)
545				}
546				ts.Fatalf("have %d matches for %#q, want %d", count, pattern, n)
547			}
548		}
549	}
550}
551
552// unix2DOS returns data with UNIX line endings converted to DOS line endings.
553func unix2DOS(data []byte) ([]byte, error) {
554	sb := &strings.Builder{}
555	s := bufio.NewScanner(bytes.NewReader(data))
556	for s.Scan() {
557		if _, err := sb.Write(s.Bytes()); err != nil {
558			return nil, err
559		}
560		if _, err := sb.WriteString("\r\n"); err != nil {
561			return nil, err
562		}
563	}
564	if err := s.Err(); err != nil {
565		return nil, err
566	}
567	return []byte(sb.String()), nil
568}
569