...

Source file src/github.com/rogpeppe/go-internal/testscript/testscript_test.go

Documentation: github.com/rogpeppe/go-internal/testscript

     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  package testscript
     6  
     7  import (
     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  
    25  func printArgs() int {
    26  	fmt.Printf("%q\n", os.Args)
    27  	return 0
    28  }
    29  
    30  func 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  
    41  func exitWithStatus() int {
    42  	n, _ := strconv.Atoi(os.Args[1])
    43  	return n
    44  }
    45  
    46  func 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, 0o666); err != nil {
    53  		fmt.Println(err)
    54  		return 1
    55  	}
    56  	<-c
    57  	fmt.Println("caught interrupt")
    58  	return 0
    59  }
    60  
    61  func terminalPrompt() int {
    62  	tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
    63  	if err != nil {
    64  		fmt.Println(err)
    65  		return 1
    66  	}
    67  	tty.WriteString("The magic words are: ")
    68  	var words string
    69  	fmt.Fscanln(tty, &words)
    70  	if words != "SQUEAMISHOSSIFRAGE" {
    71  		fmt.Println(words)
    72  		return 42
    73  	}
    74  	return 0
    75  }
    76  
    77  func TestMain(m *testing.M) {
    78  	timeSince = func(t time.Time) time.Duration {
    79  		return 0
    80  	}
    81  
    82  	showVerboseEnv = false
    83  	os.Exit(RunMain(m, map[string]func() int{
    84  		"printargs":      printArgs,
    85  		"fprintargs":     fprintArgs,
    86  		"status":         exitWithStatus,
    87  		"signalcatcher":  signalCatcher,
    88  		"terminalprompt": terminalPrompt,
    89  	}))
    90  }
    91  
    92  func TestCRLFInput(t *testing.T) {
    93  	td, err := ioutil.TempDir("", "")
    94  	if err != nil {
    95  		t.Fatalf("failed to create TempDir: %v", err)
    96  	}
    97  	defer func() {
    98  		os.RemoveAll(td)
    99  	}()
   100  	tf := filepath.Join(td, "script.txt")
   101  	contents := []byte("exists output.txt\r\n-- output.txt --\r\noutput contents")
   102  	if err := ioutil.WriteFile(tf, contents, 0o644); err != nil {
   103  		t.Fatalf("failed to write to %v: %v", tf, err)
   104  	}
   105  	t.Run("_", func(t *testing.T) {
   106  		Run(t, Params{Dir: td})
   107  	})
   108  }
   109  
   110  func TestEnv(t *testing.T) {
   111  	e := &Env{
   112  		Vars: []string{
   113  			"HOME=/no-home",
   114  			"PATH=/usr/bin",
   115  			"PATH=/usr/bin:/usr/local/bin",
   116  			"INVALID",
   117  		},
   118  	}
   119  
   120  	if got, want := e.Getenv("HOME"), "/no-home"; got != want {
   121  		t.Errorf("e.Getenv(\"HOME\") == %q, want %q", got, want)
   122  	}
   123  
   124  	e.Setenv("HOME", "/home/user")
   125  	if got, want := e.Getenv("HOME"), "/home/user"; got != want {
   126  		t.Errorf(`e.Getenv("HOME") == %q, want %q`, got, want)
   127  	}
   128  
   129  	if got, want := e.Getenv("PATH"), "/usr/bin:/usr/local/bin"; got != want {
   130  		t.Errorf(`e.Getenv("PATH") == %q, want %q`, got, want)
   131  	}
   132  
   133  	if got, want := e.Getenv("INVALID"), ""; got != want {
   134  		t.Errorf(`e.Getenv("INVALID") == %q, want %q`, got, want)
   135  	}
   136  
   137  	for _, key := range []string{
   138  		"",
   139  		"=",
   140  		"key=invalid",
   141  	} {
   142  		var panicValue interface{}
   143  		func() {
   144  			defer func() {
   145  				panicValue = recover()
   146  			}()
   147  			e.Setenv(key, "")
   148  		}()
   149  		if panicValue == nil {
   150  			t.Errorf("e.Setenv(%q) did not panic, want panic", key)
   151  		}
   152  	}
   153  }
   154  
   155  func TestSetupFailure(t *testing.T) {
   156  	dir := t.TempDir()
   157  	if err := os.WriteFile(filepath.Join(dir, "foo.txt"), nil, 0o666); err != nil {
   158  		t.Fatal(err)
   159  	}
   160  	ft := &fakeT{}
   161  	func() {
   162  		defer catchAbort()
   163  		RunT(ft, Params{
   164  			Dir: dir,
   165  			Setup: func(*Env) error {
   166  				return fmt.Errorf("some failure")
   167  			},
   168  		})
   169  	}()
   170  	if !ft.failed {
   171  		t.Fatal("test should have failed because of setup failure")
   172  	}
   173  
   174  	want := regexp.MustCompile(`^FAIL: .*: some failure\n$`)
   175  	if got := ft.log.String(); !want.MatchString(got) {
   176  		t.Fatalf("expected msg to match `%v`; got:\n%q", want, got)
   177  	}
   178  }
   179  
   180  func TestScripts(t *testing.T) {
   181  	// TODO set temp directory.
   182  	testDeferCount := 0
   183  	Run(t, Params{
   184  		UpdateScripts: os.Getenv("TESTSCRIPT_UPDATE") != "",
   185  		Dir:           "testdata",
   186  		Cmds: map[string]func(ts *TestScript, neg bool, args []string){
   187  			"setSpecialVal":    setSpecialVal,
   188  			"ensureSpecialVal": ensureSpecialVal,
   189  			"interrupt":        interrupt,
   190  			"waitfile":         waitFile,
   191  			"testdefer": func(ts *TestScript, neg bool, args []string) {
   192  				testDeferCount++
   193  				n := testDeferCount
   194  				ts.Defer(func() {
   195  					if testDeferCount != n {
   196  						t.Errorf("defers not run in reverse order; got %d want %d", testDeferCount, n)
   197  					}
   198  					testDeferCount--
   199  				})
   200  			},
   201  			"setup-filenames": func(ts *TestScript, neg bool, want []string) {
   202  				got := ts.Value("setupFilenames")
   203  				if !reflect.DeepEqual(want, got) {
   204  					ts.Fatalf("setup did not see expected files; got %q want %q", got, want)
   205  				}
   206  			},
   207  			"test-values": func(ts *TestScript, neg bool, args []string) {
   208  				if ts.Value("somekey") != 1234 {
   209  					ts.Fatalf("test-values did not see expected value")
   210  				}
   211  				if ts.Value("t").(T) != ts.t {
   212  					ts.Fatalf("test-values did not see expected t")
   213  				}
   214  				if _, ok := ts.Value("t").(testing.TB); !ok {
   215  					ts.Fatalf("test-values t does not implement testing.TB")
   216  				}
   217  			},
   218  			"testreadfile": func(ts *TestScript, neg bool, args []string) {
   219  				if len(args) != 1 {
   220  					ts.Fatalf("testreadfile <filename>")
   221  				}
   222  				got := ts.ReadFile(args[0])
   223  				want := args[0] + "\n"
   224  				if got != want {
   225  					ts.Fatalf("reading %q; got %q want %q", args[0], got, want)
   226  				}
   227  			},
   228  			"testscript": func(ts *TestScript, neg bool, args []string) {
   229  				// Run testscript in testscript. Oooh! Meta!
   230  				fset := flag.NewFlagSet("testscript", flag.ContinueOnError)
   231  				fUpdate := fset.Bool("update", false, "update scripts when cmp fails")
   232  				fExplicitExec := fset.Bool("explicit-exec", false, "require explicit use of exec for commands")
   233  				fUniqueNames := fset.Bool("unique-names", false, "require unique names in txtar archive")
   234  				fVerbose := fset.Bool("v", false, "be verbose with output")
   235  				fContinue := fset.Bool("continue", false, "continue on error")
   236  				if err := fset.Parse(args); err != nil {
   237  					ts.Fatalf("failed to parse args for testscript: %v", err)
   238  				}
   239  				if fset.NArg() != 1 {
   240  					ts.Fatalf("testscript [-v] [-continue] [-update] [-explicit-exec] <dir>")
   241  				}
   242  				dir := fset.Arg(0)
   243  				t := &fakeT{verbose: *fVerbose}
   244  				func() {
   245  					defer catchAbort()
   246  					RunT(t, Params{
   247  						Dir:                 ts.MkAbs(dir),
   248  						UpdateScripts:       *fUpdate,
   249  						RequireExplicitExec: *fExplicitExec,
   250  						RequireUniqueNames:  *fUniqueNames,
   251  						Cmds: map[string]func(ts *TestScript, neg bool, args []string){
   252  							"some-param-cmd": func(ts *TestScript, neg bool, args []string) {
   253  							},
   254  							"echoandexit": echoandexit,
   255  						},
   256  						ContinueOnError: *fContinue,
   257  					})
   258  				}()
   259  				stdout := t.log.String()
   260  				stdout = strings.ReplaceAll(stdout, ts.workdir, "$WORK")
   261  				fmt.Fprint(ts.Stdout(), stdout)
   262  				if neg {
   263  					if !t.failed {
   264  						ts.Fatalf("testscript unexpectedly succeeded")
   265  					}
   266  					return
   267  				}
   268  				if t.failed {
   269  					ts.Fatalf("testscript unexpectedly failed with errors: %q", &t.log)
   270  				}
   271  			},
   272  			"echoandexit": echoandexit,
   273  		},
   274  		Setup: func(env *Env) error {
   275  			infos, err := ioutil.ReadDir(env.WorkDir)
   276  			if err != nil {
   277  				return fmt.Errorf("cannot read workdir: %v", err)
   278  			}
   279  			var setupFilenames []string
   280  			for _, info := range infos {
   281  				setupFilenames = append(setupFilenames, info.Name())
   282  			}
   283  			env.Values["setupFilenames"] = setupFilenames
   284  			env.Values["somekey"] = 1234
   285  			env.Values["t"] = env.T()
   286  			env.Vars = append(env.Vars,
   287  				"GONOSUMDB=*",
   288  			)
   289  			return nil
   290  		},
   291  	})
   292  	if testDeferCount != 0 {
   293  		t.Fatalf("defer mismatch; got %d want 0", testDeferCount)
   294  	}
   295  	// TODO check that the temp directory has been removed.
   296  }
   297  
   298  func echoandexit(ts *TestScript, neg bool, args []string) {
   299  	// Takes at least one argument
   300  	//
   301  	// args[0] - int that indicates the exit code of the command
   302  	// args[1] - the string to echo to stdout if non-empty
   303  	// args[2] - the string to echo to stderr if non-empty
   304  	if len(args) == 0 || len(args) > 3 {
   305  		ts.Fatalf("echoandexit takes at least one and at most three arguments")
   306  	}
   307  	if neg {
   308  		ts.Fatalf("neg means nothing for echoandexit")
   309  	}
   310  	exitCode, err := strconv.ParseInt(args[0], 10, 64)
   311  	if err != nil {
   312  		ts.Fatalf("failed to parse exit code from %q: %v", args[0], err)
   313  	}
   314  	if len(args) > 1 && args[1] != "" {
   315  		fmt.Fprint(ts.Stdout(), args[1])
   316  	}
   317  	if len(args) > 2 && args[2] != "" {
   318  		fmt.Fprint(ts.Stderr(), args[2])
   319  	}
   320  	if exitCode != 0 {
   321  		ts.Fatalf("told to exit with code %d", exitCode)
   322  	}
   323  }
   324  
   325  // TestTestwork tests that using the flag -testwork will make sure the work dir isn't removed
   326  // after the test is done. It uses an empty testscript file that doesn't do anything.
   327  func TestTestwork(t *testing.T) {
   328  	out, err := exec.Command("go", "test", ".", "-testwork", "-v", "-run", "TestScripts/^nothing$").CombinedOutput()
   329  	if err != nil {
   330  		t.Fatal(err)
   331  	}
   332  
   333  	re := regexp.MustCompile(`\s+WORK=(\S+)`)
   334  	match := re.FindAllStringSubmatch(string(out), -1)
   335  
   336  	// Ensure that there is only one line with one match
   337  	if len(match) != 1 || len(match[0]) != 2 {
   338  		t.Fatalf("failed to extract WORK directory")
   339  	}
   340  
   341  	var fi os.FileInfo
   342  	if fi, err = os.Stat(match[0][1]); err != nil {
   343  		t.Fatalf("failed to stat expected work directory %v: %v", match[0][1], err)
   344  	}
   345  
   346  	if !fi.IsDir() {
   347  		t.Fatalf("expected persisted workdir is not a directory: %v", match[0][1])
   348  	}
   349  }
   350  
   351  // TestWorkdirRoot tests that a non zero value in Params.WorkdirRoot is honoured
   352  func TestWorkdirRoot(t *testing.T) {
   353  	td, err := ioutil.TempDir("", "")
   354  	if err != nil {
   355  		t.Fatalf("failed to create temp dir: %v", err)
   356  	}
   357  	defer os.RemoveAll(td)
   358  	params := Params{
   359  		Dir:         filepath.Join("testdata", "nothing"),
   360  		WorkdirRoot: td,
   361  	}
   362  	// Run as a sub-test so that this call blocks until the sub-tests created by
   363  	// calling Run (which themselves call t.Parallel) complete.
   364  	t.Run("run tests", func(t *testing.T) {
   365  		Run(t, params)
   366  	})
   367  	// Verify that we have a single go-test-script-* named directory
   368  	files, err := filepath.Glob(filepath.Join(td, "script-nothing", "README.md"))
   369  	if err != nil {
   370  		t.Fatal(err)
   371  	}
   372  	if len(files) != 1 {
   373  		t.Fatalf("unexpected files found for kept files; got %q", files)
   374  	}
   375  }
   376  
   377  // TestBadDir verifies that invoking testscript with a directory that either
   378  // does not exist or that contains no *.txt scripts fails the test
   379  func TestBadDir(t *testing.T) {
   380  	ft := new(fakeT)
   381  	func() {
   382  		defer catchAbort()
   383  		RunT(ft, Params{
   384  			Dir: "thiswillnevermatch",
   385  		})
   386  	}()
   387  	want := regexp.MustCompile(`no txtar nor txt scripts found in dir thiswillnevermatch`)
   388  	if got := ft.log.String(); !want.MatchString(got) {
   389  		t.Fatalf("expected msg to match `%v`; got:\n%v", want, got)
   390  	}
   391  }
   392  
   393  // catchAbort catches the panic raised by fakeT.FailNow.
   394  func catchAbort() {
   395  	if err := recover(); err != nil && err != errAbort {
   396  		panic(err)
   397  	}
   398  }
   399  
   400  func TestUNIX2DOS(t *testing.T) {
   401  	for data, want := range map[string]string{
   402  		"":         "",           // Preserve empty files.
   403  		"\n":       "\r\n",       // Convert LF to CRLF in a file containing a single empty line.
   404  		"\r\n":     "\r\n",       // Preserve CRLF in a single line file.
   405  		"a":        "a\r\n",      // Append CRLF to a single line file with no line terminator.
   406  		"a\n":      "a\r\n",      // Convert LF to CRLF in a file containing a single non-empty line.
   407  		"a\r\n":    "a\r\n",      // Preserve CRLF in a file containing a single non-empty line.
   408  		"a\nb\n":   "a\r\nb\r\n", // Convert LF to CRLF in multiline UNIX file.
   409  		"a\r\nb\n": "a\r\nb\r\n", // Convert LF to CRLF in a file containing a mix of UNIX and DOS lines.
   410  		"a\nb\r\n": "a\r\nb\r\n", // Convert LF to CRLF in a file containing a mix of UNIX and DOS lines.
   411  	} {
   412  		if got, err := unix2DOS([]byte(data)); err != nil || !bytes.Equal(got, []byte(want)) {
   413  			t.Errorf("unix2DOS(%q) == %q, %v, want %q, nil", data, got, err, want)
   414  		}
   415  	}
   416  }
   417  
   418  func setSpecialVal(ts *TestScript, neg bool, args []string) {
   419  	ts.Setenv("SPECIALVAL", "42")
   420  }
   421  
   422  func ensureSpecialVal(ts *TestScript, neg bool, args []string) {
   423  	want := "42"
   424  	if got := ts.Getenv("SPECIALVAL"); got != want {
   425  		ts.Fatalf("expected SPECIALVAL to be %q; got %q", want, got)
   426  	}
   427  }
   428  
   429  // interrupt interrupts the current background command.
   430  // Note that this will not work under Windows.
   431  func interrupt(ts *TestScript, neg bool, args []string) {
   432  	if neg {
   433  		ts.Fatalf("interrupt does not support neg")
   434  	}
   435  	if len(args) > 0 {
   436  		ts.Fatalf("unexpected args found")
   437  	}
   438  	bg := ts.BackgroundCmds()
   439  	if got, want := len(bg), 1; got != want {
   440  		ts.Fatalf("unexpected background cmd count; got %d want %d", got, want)
   441  	}
   442  	bg[0].Process.Signal(os.Interrupt)
   443  }
   444  
   445  func waitFile(ts *TestScript, neg bool, args []string) {
   446  	if neg {
   447  		ts.Fatalf("waitfile does not support neg")
   448  	}
   449  	if len(args) != 1 {
   450  		ts.Fatalf("usage: waitfile file")
   451  	}
   452  	path := ts.MkAbs(args[0])
   453  	for i := 0; i < 100; i++ {
   454  		_, err := os.Stat(path)
   455  		if err == nil {
   456  			return
   457  		}
   458  		if !os.IsNotExist(err) {
   459  			ts.Fatalf("unexpected stat error: %v", err)
   460  		}
   461  		time.Sleep(10 * time.Millisecond)
   462  	}
   463  	ts.Fatalf("timed out waiting for %q to be created", path)
   464  }
   465  
   466  type fakeT struct {
   467  	log     strings.Builder
   468  	verbose bool
   469  	failed  bool
   470  }
   471  
   472  var errAbort = errors.New("abort test")
   473  
   474  func (t *fakeT) Skip(args ...interface{}) {
   475  	panic(errAbort)
   476  }
   477  
   478  func (t *fakeT) Fatal(args ...interface{}) {
   479  	t.Log(args...)
   480  	t.FailNow()
   481  }
   482  
   483  func (t *fakeT) Parallel() {}
   484  
   485  func (t *fakeT) Log(args ...interface{}) {
   486  	fmt.Fprint(&t.log, args...)
   487  }
   488  
   489  func (t *fakeT) FailNow() {
   490  	t.failed = true
   491  	panic(errAbort)
   492  }
   493  
   494  func (t *fakeT) Run(name string, f func(T)) {
   495  	f(t)
   496  }
   497  
   498  func (t *fakeT) Verbose() bool {
   499  	return t.verbose
   500  }
   501  
   502  func (t *fakeT) Failed() bool {
   503  	return t.failed
   504  }
   505  

View as plain text