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