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	"bytes"
9	"errors"
10	"flag"
11	"fmt"
12	"io/ioutil"
13	"os"
14	"os/exec"
15	"os/signal"
16	"path/filepath"
17	"reflect"
18	"regexp"
19	"strconv"
20	"strings"
21	"testing"
22	"time"
23)
24
25func printArgs() int {
26	fmt.Printf("%q\n", os.Args)
27	return 0
28}
29
30func fprintArgs() int {
31	s := strings.Join(os.Args[2:], " ")
32	switch os.Args[1] {
33	case "stdout":
34		fmt.Println(s)
35	case "stderr":
36		fmt.Fprintln(os.Stderr, s)
37	}
38	return 0
39}
40
41func exitWithStatus() int {
42	n, _ := strconv.Atoi(os.Args[1])
43	return n
44}
45
46func signalCatcher() int {
47	// Note: won't work under Windows.
48	c := make(chan os.Signal, 1)
49	signal.Notify(c, os.Interrupt)
50	// Create a file so that the test can know that
51	// we will catch the signal.
52	if err := ioutil.WriteFile("catchsignal", nil, 0666); err != nil {
53		fmt.Println(err)
54		return 1
55	}
56	<-c
57	fmt.Println("caught interrupt")
58	return 0
59}
60
61func TestMain(m *testing.M) {
62	os.Exit(RunMain(m, map[string]func() int{
63		"printargs":     printArgs,
64		"fprintargs":    fprintArgs,
65		"status":        exitWithStatus,
66		"signalcatcher": signalCatcher,
67	}))
68}
69
70func TestCRLFInput(t *testing.T) {
71	td, err := ioutil.TempDir("", "")
72	if err != nil {
73		t.Fatalf("failed to create TempDir: %v", err)
74	}
75	defer func() {
76		os.RemoveAll(td)
77	}()
78	tf := filepath.Join(td, "script.txt")
79	contents := []byte("exists output.txt\r\n-- output.txt --\r\noutput contents")
80	if err := ioutil.WriteFile(tf, contents, 0644); err != nil {
81		t.Fatalf("failed to write to %v: %v", tf, err)
82	}
83	t.Run("_", func(t *testing.T) {
84		Run(t, Params{Dir: td})
85	})
86}
87
88func TestEnv(t *testing.T) {
89	e := &Env{
90		Vars: []string{
91			"HOME=/no-home",
92			"PATH=/usr/bin",
93			"PATH=/usr/bin:/usr/local/bin",
94			"INVALID",
95		},
96	}
97
98	if got, want := e.Getenv("HOME"), "/no-home"; got != want {
99		t.Errorf("e.Getenv(\"HOME\") == %q, want %q", got, want)
100	}
101
102	e.Setenv("HOME", "/home/user")
103	if got, want := e.Getenv("HOME"), "/home/user"; got != want {
104		t.Errorf(`e.Getenv("HOME") == %q, want %q`, got, want)
105	}
106
107	if got, want := e.Getenv("PATH"), "/usr/bin:/usr/local/bin"; got != want {
108		t.Errorf(`e.Getenv("PATH") == %q, want %q`, got, want)
109	}
110
111	if got, want := e.Getenv("INVALID"), ""; got != want {
112		t.Errorf(`e.Getenv("INVALID") == %q, want %q`, got, want)
113	}
114
115	for _, key := range []string{
116		"",
117		"=",
118		"key=invalid",
119	} {
120		var panicValue interface{}
121		func() {
122			defer func() {
123				panicValue = recover()
124			}()
125			e.Setenv(key, "")
126		}()
127		if panicValue == nil {
128			t.Errorf("e.Setenv(%q) did not panic, want panic", key)
129		}
130	}
131}
132
133func TestScripts(t *testing.T) {
134	// TODO set temp directory.
135	testDeferCount := 0
136	Run(t, Params{
137		UpdateScripts: os.Getenv("TESTSCRIPT_UPDATE") != "",
138		Dir:           "testdata",
139		Cmds: map[string]func(ts *TestScript, neg bool, args []string){
140			"setSpecialVal":    setSpecialVal,
141			"ensureSpecialVal": ensureSpecialVal,
142			"interrupt":        interrupt,
143			"waitfile":         waitFile,
144			"testdefer": func(ts *TestScript, neg bool, args []string) {
145				testDeferCount++
146				n := testDeferCount
147				ts.Defer(func() {
148					if testDeferCount != n {
149						t.Errorf("defers not run in reverse order; got %d want %d", testDeferCount, n)
150					}
151					testDeferCount--
152				})
153			},
154			"setup-filenames": func(ts *TestScript, neg bool, want []string) {
155				got := ts.Value("setupFilenames")
156				if !reflect.DeepEqual(want, got) {
157					ts.Fatalf("setup did not see expected files; got %q want %q", got, want)
158				}
159			},
160			"test-values": func(ts *TestScript, neg bool, args []string) {
161				if ts.Value("somekey") != 1234 {
162					ts.Fatalf("test-values did not see expected value")
163				}
164				if ts.Value("t").(T) != ts.t {
165					ts.Fatalf("test-values did not see expected t")
166				}
167				if _, ok := ts.Value("t").(testing.TB); !ok {
168					ts.Fatalf("test-values t does not implement testing.TB")
169				}
170			},
171			"testreadfile": func(ts *TestScript, neg bool, args []string) {
172				if len(args) != 1 {
173					ts.Fatalf("testreadfile <filename>")
174				}
175				got := ts.ReadFile(args[0])
176				want := args[0] + "\n"
177				if got != want {
178					ts.Fatalf("reading %q; got %q want %q", args[0], got, want)
179				}
180			},
181			"testscript": func(ts *TestScript, neg bool, args []string) {
182				// Run testscript in testscript. Oooh! Meta!
183				fset := flag.NewFlagSet("testscript", flag.ContinueOnError)
184				fUpdate := fset.Bool("update", false, "update scripts when cmp fails")
185				fVerbose := fset.Bool("verbose", false, "be verbose with output")
186				if err := fset.Parse(args); err != nil {
187					ts.Fatalf("failed to parse args for testscript: %v", err)
188				}
189				if fset.NArg() != 1 {
190					ts.Fatalf("testscript [-verbose] [-update] <dir>")
191				}
192				dir := fset.Arg(0)
193				t := &fakeT{ts: ts, verbose: *fVerbose}
194				func() {
195					defer func() {
196						if err := recover(); err != nil {
197							if err != errAbort {
198								panic(err)
199							}
200						}
201					}()
202					RunT(t, Params{
203						Dir:           ts.MkAbs(dir),
204						UpdateScripts: *fUpdate,
205					})
206				}()
207				ts.stdout = strings.Replace(t.log.String(), ts.workdir, "$WORK", -1)
208				if neg {
209					if len(t.failMsgs) == 0 {
210						ts.Fatalf("testscript unexpectedly succeeded")
211					}
212					return
213				}
214				if len(t.failMsgs) > 0 {
215					ts.Fatalf("testscript unexpectedly failed with errors: %q", t.failMsgs)
216				}
217			},
218		},
219		Setup: func(env *Env) error {
220			infos, err := ioutil.ReadDir(env.WorkDir)
221			if err != nil {
222				return fmt.Errorf("cannot read workdir: %v", err)
223			}
224			var setupFilenames []string
225			for _, info := range infos {
226				setupFilenames = append(setupFilenames, info.Name())
227			}
228			env.Values["setupFilenames"] = setupFilenames
229			env.Values["somekey"] = 1234
230			env.Values["t"] = env.T()
231			env.Vars = append(env.Vars,
232				"GONOSUMDB=*",
233			)
234			return nil
235		},
236	})
237	if testDeferCount != 0 {
238		t.Fatalf("defer mismatch; got %d want 0", testDeferCount)
239	}
240	// TODO check that the temp directory has been removed.
241}
242
243// TestTestwork tests that using the flag -testwork will make sure the work dir isn't removed
244// after the test is done. It uses an empty testscript file that doesn't do anything.
245func TestTestwork(t *testing.T) {
246	out, err := exec.Command("go", "test", ".", "-testwork", "-v", "-run", "TestScripts/^nothing$").CombinedOutput()
247	if err != nil {
248		t.Fatal(err)
249	}
250
251	re := regexp.MustCompile(`\s+WORK=(\S+)`)
252	match := re.FindAllStringSubmatch(string(out), -1)
253
254	// Ensure that there is only one line with one match
255	if len(match) != 1 || len(match[0]) != 2 {
256		t.Fatalf("failed to extract WORK directory")
257	}
258
259	var fi os.FileInfo
260	if fi, err = os.Stat(match[0][1]); err != nil {
261		t.Fatalf("failed to stat expected work directory %v: %v", match[0][1], err)
262	}
263
264	if !fi.IsDir() {
265		t.Fatalf("expected persisted workdir is not a directory: %v", match[0][1])
266	}
267}
268
269// TestWorkdirRoot tests that a non zero value in Params.WorkdirRoot is honoured
270func TestWorkdirRoot(t *testing.T) {
271	td, err := ioutil.TempDir("", "")
272	if err != nil {
273		t.Fatalf("failed to create temp dir: %v", err)
274	}
275	defer os.RemoveAll(td)
276	params := Params{
277		Dir:         filepath.Join("testdata", "nothing"),
278		WorkdirRoot: td,
279	}
280	// Run as a sub-test so that this call blocks until the sub-tests created by
281	// calling Run (which themselves call t.Parallel) complete.
282	t.Run("run tests", func(t *testing.T) {
283		Run(t, params)
284	})
285	// Verify that we have a single go-test-script-* named directory
286	files, err := filepath.Glob(filepath.Join(td, "script-nothing", "README.md"))
287	if err != nil {
288		t.Fatal(err)
289	}
290	if len(files) != 1 {
291		t.Fatalf("unexpected files found for kept files; got %q", files)
292	}
293}
294
295// TestBadDir verifies that invoking testscript with a directory that either
296// does not exist or that contains no *.txt scripts fails the test
297func TestBadDir(t *testing.T) {
298	ft := new(fakeT)
299	func() {
300		defer func() {
301			if err := recover(); err != nil {
302				if err != errAbort {
303					panic(err)
304				}
305			}
306		}()
307		RunT(ft, Params{
308			Dir: "thiswillnevermatch",
309		})
310	}()
311	wantCount := 1
312	if got := len(ft.failMsgs); got != wantCount {
313		t.Fatalf("expected %v fail message; got %v", wantCount, got)
314	}
315	wantMsg := regexp.MustCompile(`no scripts found matching glob: thiswillnevermatch[/\\]\*\.txt`)
316	if got := ft.failMsgs[0]; !wantMsg.MatchString(got) {
317		t.Fatalf("expected msg to match `%v`; got:\n%v", wantMsg, got)
318	}
319}
320
321func TestUNIX2DOS(t *testing.T) {
322	for data, want := range map[string]string{
323		"":         "",           // Preserve empty files.
324		"\n":       "\r\n",       // Convert LF to CRLF in a file containing a single empty line.
325		"\r\n":     "\r\n",       // Preserve CRLF in a single line file.
326		"a":        "a\r\n",      // Append CRLF to a single line file with no line terminator.
327		"a\n":      "a\r\n",      // Convert LF to CRLF in a file containing a single non-empty line.
328		"a\r\n":    "a\r\n",      // Preserve CRLF in a file containing a single non-empty line.
329		"a\nb\n":   "a\r\nb\r\n", // Convert LF to CRLF in multiline UNIX file.
330		"a\r\nb\n": "a\r\nb\r\n", // Convert LF to CRLF in a file containing a mix of UNIX and DOS lines.
331		"a\nb\r\n": "a\r\nb\r\n", // Convert LF to CRLF in a file containing a mix of UNIX and DOS lines.
332	} {
333		if got, err := unix2DOS([]byte(data)); err != nil || !bytes.Equal(got, []byte(want)) {
334			t.Errorf("unix2DOS(%q) == %q, %v, want %q, nil", data, got, err, want)
335		}
336	}
337}
338
339func setSpecialVal(ts *TestScript, neg bool, args []string) {
340	ts.Setenv("SPECIALVAL", "42")
341}
342
343func ensureSpecialVal(ts *TestScript, neg bool, args []string) {
344	want := "42"
345	if got := ts.Getenv("SPECIALVAL"); got != want {
346		ts.Fatalf("expected SPECIALVAL to be %q; got %q", want, got)
347	}
348}
349
350// interrupt interrupts the current background command.
351// Note that this will not work under Windows.
352func interrupt(ts *TestScript, neg bool, args []string) {
353	if neg {
354		ts.Fatalf("interrupt does not support neg")
355	}
356	if len(args) > 0 {
357		ts.Fatalf("unexpected args found")
358	}
359	bg := ts.BackgroundCmds()
360	if got, want := len(bg), 1; got != want {
361		ts.Fatalf("unexpected background cmd count; got %d want %d", got, want)
362	}
363	bg[0].Process.Signal(os.Interrupt)
364}
365
366func waitFile(ts *TestScript, neg bool, args []string) {
367	if neg {
368		ts.Fatalf("waitfile does not support neg")
369	}
370	if len(args) != 1 {
371		ts.Fatalf("usage: waitfile file")
372	}
373	path := ts.MkAbs(args[0])
374	for i := 0; i < 100; i++ {
375		_, err := os.Stat(path)
376		if err == nil {
377			return
378		}
379		if !os.IsNotExist(err) {
380			ts.Fatalf("unexpected stat error: %v", err)
381		}
382		time.Sleep(10 * time.Millisecond)
383	}
384	ts.Fatalf("timed out waiting for %q to be created", path)
385}
386
387type fakeT struct {
388	ts       *TestScript
389	log      bytes.Buffer
390	failMsgs []string
391	verbose  bool
392}
393
394var errAbort = errors.New("abort test")
395
396func (t *fakeT) Skip(args ...interface{}) {
397	panic(errAbort)
398}
399
400func (t *fakeT) Fatal(args ...interface{}) {
401	t.failMsgs = append(t.failMsgs, fmt.Sprint(args...))
402	panic(errAbort)
403}
404
405func (t *fakeT) Parallel() {}
406
407func (t *fakeT) Log(args ...interface{}) {
408	fmt.Fprint(&t.log, args...)
409}
410
411func (t *fakeT) FailNow() {
412	t.Fatal("failed")
413}
414
415func (t *fakeT) Run(name string, f func(T)) {
416	f(t)
417}
418
419func (t *fakeT) Verbose() bool {
420	return t.verbose
421}
422