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 main_test 9 10import ( 11 "bytes" 12 "context" 13 "errors" 14 "fmt" 15 "go/build" 16 "internal/testenv" 17 "io/fs" 18 "os" 19 "os/exec" 20 "path/filepath" 21 "regexp" 22 "runtime" 23 "strconv" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 "cmd/go/internal/cfg" 30 "cmd/go/internal/imports" 31 "cmd/go/internal/par" 32 "cmd/go/internal/robustio" 33 "cmd/go/internal/txtar" 34 "cmd/go/internal/work" 35 "cmd/internal/objabi" 36 "cmd/internal/sys" 37) 38 39// TestScript runs the tests in testdata/script/*.txt. 40func TestScript(t *testing.T) { 41 testenv.MustHaveGoBuild(t) 42 testenv.SkipIfShortAndSlow(t) 43 44 files, err := filepath.Glob("testdata/script/*.txt") 45 if err != nil { 46 t.Fatal(err) 47 } 48 for _, file := range files { 49 file := file 50 name := strings.TrimSuffix(filepath.Base(file), ".txt") 51 t.Run(name, func(t *testing.T) { 52 t.Parallel() 53 ts := &testScript{t: t, name: name, file: file} 54 ts.setup() 55 if !*testWork { 56 defer removeAll(ts.workdir) 57 } 58 ts.run() 59 }) 60 } 61} 62 63// A testScript holds execution state for a single test script. 64type testScript struct { 65 t *testing.T 66 workdir string // temporary work dir ($WORK) 67 log bytes.Buffer // test execution log (printed at end of test) 68 mark int // offset of next log truncation 69 cd string // current directory during test execution; initially $WORK/gopath/src 70 name string // short name of test ("foo") 71 file string // full file name ("testdata/script/foo.txt") 72 lineno int // line number currently executing 73 line string // line currently executing 74 env []string // environment list (for os/exec) 75 envMap map[string]string // environment mapping (matches env) 76 stdout string // standard output from last 'go' command; for 'stdout' command 77 stderr string // standard error from last 'go' command; for 'stderr' command 78 stopped bool // test wants to stop early 79 start time.Time // time phase started 80 background []*backgroundCmd // backgrounded 'exec' and 'go' commands 81} 82 83type backgroundCmd struct { 84 want simpleStatus 85 args []string 86 cancel context.CancelFunc 87 done <-chan struct{} 88 err error 89 stdout, stderr strings.Builder 90} 91 92type simpleStatus string 93 94const ( 95 success simpleStatus = "" 96 failure simpleStatus = "!" 97 successOrFailure simpleStatus = "?" 98) 99 100var extraEnvKeys = []string{ 101 "SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210 102 "WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711 103 "LD_LIBRARY_PATH", // must be preserved on Unix systems to find shared libraries 104 "CC", // don't lose user settings when invoking cgo 105 "GO_TESTING_GOTOOLS", // for gccgo testing 106 "GCCGO", // for gccgo testing 107 "GCCGOTOOLDIR", // for gccgo testing 108} 109 110// setup sets up the test execution temporary directory and environment. 111func (ts *testScript) setup() { 112 StartProxy() 113 ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name) 114 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777)) 115 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777)) 116 ts.cd = filepath.Join(ts.workdir, "gopath/src") 117 ts.env = []string{ 118 "WORK=" + ts.workdir, // must be first for ts.abbrev 119 "PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"), 120 homeEnvName() + "=/no-home", 121 "CCACHE_DISABLE=1", // ccache breaks with non-existent HOME 122 "GOARCH=" + runtime.GOARCH, 123 "GOCACHE=" + testGOCACHE, 124 "GODEBUG=" + os.Getenv("GODEBUG"), 125 "GOEXE=" + cfg.ExeSuffix, 126 "GOEXPSTRING=" + objabi.Expstring()[2:], 127 "GOOS=" + runtime.GOOS, 128 "GOPATH=" + filepath.Join(ts.workdir, "gopath"), 129 "GOPROXY=" + proxyURL, 130 "GOPRIVATE=", 131 "GOROOT=" + testGOROOT, 132 "GOROOT_FINAL=" + os.Getenv("GOROOT_FINAL"), // causes spurious rebuilds and breaks the "stale" built-in if not propagated 133 "TESTGO_GOROOT=" + testGOROOT, 134 "GOSUMDB=" + testSumDBVerifierKey, 135 "GONOPROXY=", 136 "GONOSUMDB=", 137 "GOVCS=*:all", 138 "PWD=" + ts.cd, 139 tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"), 140 "devnull=" + os.DevNull, 141 "goversion=" + goVersion(ts), 142 ":=" + string(os.PathListSeparator), 143 } 144 if !testenv.HasExternalNetwork() { 145 ts.env = append(ts.env, "TESTGONETWORK=panic", "TESTGOVCS=panic") 146 } 147 148 if runtime.GOOS == "plan9" { 149 ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path")) 150 } 151 152 for _, key := range extraEnvKeys { 153 if val := os.Getenv(key); val != "" { 154 ts.env = append(ts.env, key+"="+val) 155 } 156 } 157 158 ts.envMap = make(map[string]string) 159 for _, kv := range ts.env { 160 if i := strings.Index(kv, "="); i >= 0 { 161 ts.envMap[kv[:i]] = kv[i+1:] 162 } 163 } 164} 165 166// goVersion returns the current Go version. 167func goVersion(ts *testScript) string { 168 tags := build.Default.ReleaseTags 169 version := tags[len(tags)-1] 170 if !regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`).MatchString(version) { 171 ts.fatalf("invalid go version %q", version) 172 } 173 return version[2:] 174} 175 176var execCache par.Cache 177 178// run runs the test script. 179func (ts *testScript) run() { 180 // Truncate log at end of last phase marker, 181 // discarding details of successful phase. 182 rewind := func() { 183 if !testing.Verbose() { 184 ts.log.Truncate(ts.mark) 185 } 186 } 187 188 // Insert elapsed time for phase at end of phase marker 189 markTime := func() { 190 if ts.mark > 0 && !ts.start.IsZero() { 191 afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...) 192 ts.log.Truncate(ts.mark - 1) // cut \n and afterMark 193 fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds()) 194 ts.log.Write(afterMark) 195 } 196 ts.start = time.Time{} 197 } 198 199 defer func() { 200 // On a normal exit from the test loop, background processes are cleaned up 201 // before we print PASS. If we return early (e.g., due to a test failure), 202 // don't print anything about the processes that were still running. 203 for _, bg := range ts.background { 204 bg.cancel() 205 } 206 for _, bg := range ts.background { 207 <-bg.done 208 } 209 ts.background = nil 210 211 markTime() 212 // Flush testScript log to testing.T log. 213 ts.t.Log("\n" + ts.abbrev(ts.log.String())) 214 }() 215 216 // Unpack archive. 217 a, err := txtar.ParseFile(ts.file) 218 ts.check(err) 219 for _, f := range a.Files { 220 name := ts.mkabs(ts.expand(f.Name, false)) 221 ts.check(os.MkdirAll(filepath.Dir(name), 0777)) 222 ts.check(os.WriteFile(name, f.Data, 0666)) 223 } 224 225 // With -v or -testwork, start log with full environment. 226 if *testWork || testing.Verbose() { 227 // Display environment. 228 ts.cmdEnv(success, nil) 229 fmt.Fprintf(&ts.log, "\n") 230 ts.mark = ts.log.Len() 231 } 232 233 // Run script. 234 // See testdata/script/README for documentation of script form. 235 script := string(a.Comment) 236Script: 237 for script != "" { 238 // Extract next line. 239 ts.lineno++ 240 var line string 241 if i := strings.Index(script, "\n"); i >= 0 { 242 line, script = script[:i], script[i+1:] 243 } else { 244 line, script = script, "" 245 } 246 247 // # is a comment indicating the start of new phase. 248 if strings.HasPrefix(line, "#") { 249 // If there was a previous phase, it succeeded, 250 // so rewind the log to delete its details (unless -v is in use). 251 // If nothing has happened at all since the mark, 252 // rewinding is a no-op and adding elapsed time 253 // for doing nothing is meaningless, so don't. 254 if ts.log.Len() > ts.mark { 255 rewind() 256 markTime() 257 } 258 // Print phase heading and mark start of phase output. 259 fmt.Fprintf(&ts.log, "%s\n", line) 260 ts.mark = ts.log.Len() 261 ts.start = time.Now() 262 continue 263 } 264 265 // Parse input line. Ignore blanks entirely. 266 parsed := ts.parse(line) 267 if parsed.name == "" { 268 if parsed.want != "" || len(parsed.conds) > 0 { 269 ts.fatalf("missing command") 270 } 271 continue 272 } 273 274 // Echo command to log. 275 fmt.Fprintf(&ts.log, "> %s\n", line) 276 277 for _, cond := range parsed.conds { 278 // Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short). 279 // 280 // NOTE: If you make changes here, update testdata/script/README too! 281 // 282 ok := false 283 switch cond.tag { 284 case runtime.GOOS, runtime.GOARCH, runtime.Compiler: 285 ok = true 286 case "short": 287 ok = testing.Short() 288 case "cgo": 289 ok = canCgo 290 case "msan": 291 ok = canMSan 292 case "race": 293 ok = canRace 294 case "net": 295 ok = testenv.HasExternalNetwork() 296 case "link": 297 ok = testenv.HasLink() 298 case "root": 299 ok = os.Geteuid() == 0 300 case "symlink": 301 ok = testenv.HasSymlink() 302 case "case-sensitive": 303 ok = isCaseSensitive(ts.t) 304 default: 305 if strings.HasPrefix(cond.tag, "exec:") { 306 prog := cond.tag[len("exec:"):] 307 ok = execCache.Do(prog, func() interface{} { 308 if runtime.GOOS == "plan9" && prog == "git" { 309 // The Git command is usually not the real Git on Plan 9. 310 // See https://golang.org/issues/29640. 311 return false 312 } 313 _, err := exec.LookPath(prog) 314 return err == nil 315 }).(bool) 316 break 317 } 318 if strings.HasPrefix(cond.tag, "GODEBUG:") { 319 value := strings.TrimPrefix(cond.tag, "GODEBUG:") 320 parts := strings.Split(os.Getenv("GODEBUG"), ",") 321 for _, p := range parts { 322 if strings.TrimSpace(p) == value { 323 ok = true 324 break 325 } 326 } 327 break 328 } 329 if strings.HasPrefix(cond.tag, "buildmode:") { 330 value := strings.TrimPrefix(cond.tag, "buildmode:") 331 ok = sys.BuildModeSupported(runtime.Compiler, value, runtime.GOOS, runtime.GOARCH) 332 break 333 } 334 if !imports.KnownArch[cond.tag] && !imports.KnownOS[cond.tag] && cond.tag != "gc" && cond.tag != "gccgo" { 335 ts.fatalf("unknown condition %q", cond.tag) 336 } 337 } 338 if ok != cond.want { 339 // Don't run rest of line. 340 continue Script 341 } 342 } 343 344 // Run command. 345 cmd := scriptCmds[parsed.name] 346 if cmd == nil { 347 ts.fatalf("unknown command %q", parsed.name) 348 } 349 cmd(ts, parsed.want, parsed.args) 350 351 // Command can ask script to stop early. 352 if ts.stopped { 353 // Break instead of returning, so that we check the status of any 354 // background processes and print PASS. 355 break 356 } 357 } 358 359 for _, bg := range ts.background { 360 bg.cancel() 361 } 362 ts.cmdWait(success, nil) 363 364 // Final phase ended. 365 rewind() 366 markTime() 367 if !ts.stopped { 368 fmt.Fprintf(&ts.log, "PASS\n") 369 } 370} 371 372var ( 373 onceCaseSensitive sync.Once 374 caseSensitive bool 375) 376 377func isCaseSensitive(t *testing.T) bool { 378 onceCaseSensitive.Do(func() { 379 tmpdir, err := os.MkdirTemp("", "case-sensitive") 380 if err != nil { 381 t.Fatal("failed to create directory to determine case-sensitivity:", err) 382 } 383 defer os.RemoveAll(tmpdir) 384 385 fcap := filepath.Join(tmpdir, "FILE") 386 if err := os.WriteFile(fcap, []byte{}, 0644); err != nil { 387 t.Fatal("error writing file to determine case-sensitivity:", err) 388 } 389 390 flow := filepath.Join(tmpdir, "file") 391 _, err = os.ReadFile(flow) 392 switch { 393 case err == nil: 394 caseSensitive = false 395 return 396 case os.IsNotExist(err): 397 caseSensitive = true 398 return 399 default: 400 t.Fatal("unexpected error reading file when determining case-sensitivity:", err) 401 } 402 }) 403 404 return caseSensitive 405} 406 407// scriptCmds are the script command implementations. 408// Keep list and the implementations below sorted by name. 409// 410// NOTE: If you make changes here, update testdata/script/README too! 411// 412var scriptCmds = map[string]func(*testScript, simpleStatus, []string){ 413 "addcrlf": (*testScript).cmdAddcrlf, 414 "cc": (*testScript).cmdCc, 415 "cd": (*testScript).cmdCd, 416 "chmod": (*testScript).cmdChmod, 417 "cmp": (*testScript).cmdCmp, 418 "cmpenv": (*testScript).cmdCmpenv, 419 "cp": (*testScript).cmdCp, 420 "env": (*testScript).cmdEnv, 421 "exec": (*testScript).cmdExec, 422 "exists": (*testScript).cmdExists, 423 "go": (*testScript).cmdGo, 424 "grep": (*testScript).cmdGrep, 425 "mkdir": (*testScript).cmdMkdir, 426 "rm": (*testScript).cmdRm, 427 "skip": (*testScript).cmdSkip, 428 "stale": (*testScript).cmdStale, 429 "stderr": (*testScript).cmdStderr, 430 "stdout": (*testScript).cmdStdout, 431 "stop": (*testScript).cmdStop, 432 "symlink": (*testScript).cmdSymlink, 433 "wait": (*testScript).cmdWait, 434} 435 436// When expanding shell variables for these commands, we apply regexp quoting to 437// expanded strings within the first argument. 438var regexpCmd = map[string]bool{ 439 "grep": true, 440 "stderr": true, 441 "stdout": true, 442} 443 444// addcrlf adds CRLF line endings to the named files. 445func (ts *testScript) cmdAddcrlf(want simpleStatus, args []string) { 446 if len(args) == 0 { 447 ts.fatalf("usage: addcrlf file...") 448 } 449 450 for _, file := range args { 451 file = ts.mkabs(file) 452 data, err := os.ReadFile(file) 453 ts.check(err) 454 ts.check(os.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666)) 455 } 456} 457 458// cc runs the C compiler along with platform specific options. 459func (ts *testScript) cmdCc(want simpleStatus, args []string) { 460 if len(args) < 1 || (len(args) == 1 && args[0] == "&") { 461 ts.fatalf("usage: cc args... [&]") 462 } 463 464 var b work.Builder 465 b.Init() 466 ts.cmdExec(want, append(b.GccCmd(".", ""), args...)) 467 robustio.RemoveAll(b.WorkDir) 468} 469 470// cd changes to a different directory. 471func (ts *testScript) cmdCd(want simpleStatus, args []string) { 472 if want != success { 473 ts.fatalf("unsupported: %v cd", want) 474 } 475 if len(args) != 1 { 476 ts.fatalf("usage: cd dir") 477 } 478 479 dir := args[0] 480 if !filepath.IsAbs(dir) { 481 dir = filepath.Join(ts.cd, dir) 482 } 483 info, err := os.Stat(dir) 484 if os.IsNotExist(err) { 485 ts.fatalf("directory %s does not exist", dir) 486 } 487 ts.check(err) 488 if !info.IsDir() { 489 ts.fatalf("%s is not a directory", dir) 490 } 491 ts.cd = dir 492 ts.envMap["PWD"] = dir 493 fmt.Fprintf(&ts.log, "%s\n", ts.cd) 494} 495 496// chmod changes permissions for a file or directory. 497func (ts *testScript) cmdChmod(want simpleStatus, args []string) { 498 if want != success { 499 ts.fatalf("unsupported: %v chmod", want) 500 } 501 if len(args) < 2 { 502 ts.fatalf("usage: chmod perm paths...") 503 } 504 perm, err := strconv.ParseUint(args[0], 0, 32) 505 if err != nil || perm&uint64(fs.ModePerm) != perm { 506 ts.fatalf("invalid mode: %s", args[0]) 507 } 508 for _, arg := range args[1:] { 509 path := arg 510 if !filepath.IsAbs(path) { 511 path = filepath.Join(ts.cd, arg) 512 } 513 err := os.Chmod(path, fs.FileMode(perm)) 514 ts.check(err) 515 } 516} 517 518// cmp compares two files. 519func (ts *testScript) cmdCmp(want simpleStatus, args []string) { 520 if want != success { 521 // It would be strange to say "this file can have any content except this precise byte sequence". 522 ts.fatalf("unsupported: %v cmp", want) 523 } 524 quiet := false 525 if len(args) > 0 && args[0] == "-q" { 526 quiet = true 527 args = args[1:] 528 } 529 if len(args) != 2 { 530 ts.fatalf("usage: cmp file1 file2") 531 } 532 ts.doCmdCmp(args, false, quiet) 533} 534 535// cmpenv compares two files with environment variable substitution. 536func (ts *testScript) cmdCmpenv(want simpleStatus, args []string) { 537 if want != success { 538 ts.fatalf("unsupported: %v cmpenv", want) 539 } 540 quiet := false 541 if len(args) > 0 && args[0] == "-q" { 542 quiet = true 543 args = args[1:] 544 } 545 if len(args) != 2 { 546 ts.fatalf("usage: cmpenv file1 file2") 547 } 548 ts.doCmdCmp(args, true, quiet) 549} 550 551func (ts *testScript) doCmdCmp(args []string, env, quiet bool) { 552 name1, name2 := args[0], args[1] 553 var text1, text2 string 554 if name1 == "stdout" { 555 text1 = ts.stdout 556 } else if name1 == "stderr" { 557 text1 = ts.stderr 558 } else { 559 data, err := os.ReadFile(ts.mkabs(name1)) 560 ts.check(err) 561 text1 = string(data) 562 } 563 564 data, err := os.ReadFile(ts.mkabs(name2)) 565 ts.check(err) 566 text2 = string(data) 567 568 if env { 569 text1 = ts.expand(text1, false) 570 text2 = ts.expand(text2, false) 571 } 572 573 if text1 == text2 { 574 return 575 } 576 577 if !quiet { 578 fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2)) 579 } 580 ts.fatalf("%s and %s differ", name1, name2) 581} 582 583// cp copies files, maybe eventually directories. 584func (ts *testScript) cmdCp(want simpleStatus, args []string) { 585 if len(args) < 2 { 586 ts.fatalf("usage: cp src... dst") 587 } 588 589 dst := ts.mkabs(args[len(args)-1]) 590 info, err := os.Stat(dst) 591 dstDir := err == nil && info.IsDir() 592 if len(args) > 2 && !dstDir { 593 ts.fatalf("cp: destination %s is not a directory", dst) 594 } 595 596 for _, arg := range args[:len(args)-1] { 597 var ( 598 src string 599 data []byte 600 mode fs.FileMode 601 ) 602 switch arg { 603 case "stdout": 604 src = arg 605 data = []byte(ts.stdout) 606 mode = 0666 607 case "stderr": 608 src = arg 609 data = []byte(ts.stderr) 610 mode = 0666 611 default: 612 src = ts.mkabs(arg) 613 info, err := os.Stat(src) 614 ts.check(err) 615 mode = info.Mode() & 0777 616 data, err = os.ReadFile(src) 617 ts.check(err) 618 } 619 targ := dst 620 if dstDir { 621 targ = filepath.Join(dst, filepath.Base(src)) 622 } 623 err := os.WriteFile(targ, data, mode) 624 switch want { 625 case failure: 626 if err == nil { 627 ts.fatalf("unexpected command success") 628 } 629 case success: 630 ts.check(err) 631 } 632 } 633} 634 635// env displays or adds to the environment. 636func (ts *testScript) cmdEnv(want simpleStatus, args []string) { 637 if want != success { 638 ts.fatalf("unsupported: %v env", want) 639 } 640 641 conv := func(s string) string { return s } 642 if len(args) > 0 && args[0] == "-r" { 643 conv = regexp.QuoteMeta 644 args = args[1:] 645 } 646 647 var out strings.Builder 648 if len(args) == 0 { 649 printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once 650 for _, kv := range ts.env { 651 k := kv[:strings.Index(kv, "=")] 652 if !printed[k] { 653 fmt.Fprintf(&out, "%s=%s\n", k, ts.envMap[k]) 654 } 655 } 656 } else { 657 for _, env := range args { 658 i := strings.Index(env, "=") 659 if i < 0 { 660 // Display value instead of setting it. 661 fmt.Fprintf(&out, "%s=%s\n", env, ts.envMap[env]) 662 continue 663 } 664 key, val := env[:i], conv(env[i+1:]) 665 ts.env = append(ts.env, key+"="+val) 666 ts.envMap[key] = val 667 } 668 } 669 if out.Len() > 0 || len(args) > 0 { 670 ts.stdout = out.String() 671 ts.log.WriteString(out.String()) 672 } 673} 674 675// exec runs the given command. 676func (ts *testScript) cmdExec(want simpleStatus, args []string) { 677 if len(args) < 1 || (len(args) == 1 && args[0] == "&") { 678 ts.fatalf("usage: exec program [args...] [&]") 679 } 680 681 background := false 682 if len(args) > 0 && args[len(args)-1] == "&" { 683 background = true 684 args = args[:len(args)-1] 685 } 686 687 bg, err := ts.startBackground(want, args[0], args[1:]...) 688 if err != nil { 689 ts.fatalf("unexpected error starting command: %v", err) 690 } 691 if background { 692 ts.stdout, ts.stderr = "", "" 693 ts.background = append(ts.background, bg) 694 return 695 } 696 697 <-bg.done 698 ts.stdout = bg.stdout.String() 699 ts.stderr = bg.stderr.String() 700 if ts.stdout != "" { 701 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout) 702 } 703 if ts.stderr != "" { 704 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr) 705 } 706 if bg.err != nil { 707 fmt.Fprintf(&ts.log, "[%v]\n", bg.err) 708 } 709 ts.checkCmd(bg) 710} 711 712// exists checks that the list of files exists. 713func (ts *testScript) cmdExists(want simpleStatus, args []string) { 714 if want == successOrFailure { 715 ts.fatalf("unsupported: %v exists", want) 716 } 717 var readonly, exec bool 718loop: 719 for len(args) > 0 { 720 switch args[0] { 721 case "-readonly": 722 readonly = true 723 args = args[1:] 724 case "-exec": 725 exec = true 726 args = args[1:] 727 default: 728 break loop 729 } 730 } 731 if len(args) == 0 { 732 ts.fatalf("usage: exists [-readonly] [-exec] file...") 733 } 734 735 for _, file := range args { 736 file = ts.mkabs(file) 737 info, err := os.Stat(file) 738 if err == nil && want == failure { 739 what := "file" 740 if info.IsDir() { 741 what = "directory" 742 } 743 ts.fatalf("%s %s unexpectedly exists", what, file) 744 } 745 if err != nil && want == success { 746 ts.fatalf("%s does not exist", file) 747 } 748 if err == nil && want == success && readonly && info.Mode()&0222 != 0 { 749 ts.fatalf("%s exists but is writable", file) 750 } 751 if err == nil && want == success && exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 { 752 ts.fatalf("%s exists but is not executable", file) 753 } 754 } 755} 756 757// go runs the go command. 758func (ts *testScript) cmdGo(want simpleStatus, args []string) { 759 ts.cmdExec(want, append([]string{testGo}, args...)) 760} 761 762// mkdir creates directories. 763func (ts *testScript) cmdMkdir(want simpleStatus, args []string) { 764 if want != success { 765 ts.fatalf("unsupported: %v mkdir", want) 766 } 767 if len(args) < 1 { 768 ts.fatalf("usage: mkdir dir...") 769 } 770 for _, arg := range args { 771 ts.check(os.MkdirAll(ts.mkabs(arg), 0777)) 772 } 773} 774 775// rm removes files or directories. 776func (ts *testScript) cmdRm(want simpleStatus, args []string) { 777 if want != success { 778 ts.fatalf("unsupported: %v rm", want) 779 } 780 if len(args) < 1 { 781 ts.fatalf("usage: rm file...") 782 } 783 for _, arg := range args { 784 file := ts.mkabs(arg) 785 removeAll(file) // does chmod and then attempts rm 786 ts.check(robustio.RemoveAll(file)) // report error 787 } 788} 789 790// skip marks the test skipped. 791func (ts *testScript) cmdSkip(want simpleStatus, args []string) { 792 if len(args) > 1 { 793 ts.fatalf("usage: skip [msg]") 794 } 795 if want != success { 796 ts.fatalf("unsupported: %v skip", want) 797 } 798 799 // Before we mark the test as skipped, shut down any background processes and 800 // make sure they have returned the correct status. 801 for _, bg := range ts.background { 802 bg.cancel() 803 } 804 ts.cmdWait(success, nil) 805 806 if len(args) == 1 { 807 ts.t.Skip(args[0]) 808 } 809 ts.t.Skip() 810} 811 812// stale checks that the named build targets are stale. 813func (ts *testScript) cmdStale(want simpleStatus, args []string) { 814 if len(args) == 0 { 815 ts.fatalf("usage: stale target...") 816 } 817 tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{{else}}" 818 switch want { 819 case failure: 820 tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale{{end}}" 821 case success: 822 tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}" 823 default: 824 ts.fatalf("unsupported: %v stale", want) 825 } 826 tmpl += "{{end}}" 827 goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...) 828 stdout, stderr, err := ts.exec(testGo, goArgs...) 829 if err != nil { 830 ts.fatalf("go list: %v\n%s%s", err, stdout, stderr) 831 } 832 if stdout != "" { 833 ts.fatalf("%s", stdout) 834 } 835} 836 837// stdout checks that the last go command standard output matches a regexp. 838func (ts *testScript) cmdStdout(want simpleStatus, args []string) { 839 scriptMatch(ts, want, args, ts.stdout, "stdout") 840} 841 842// stderr checks that the last go command standard output matches a regexp. 843func (ts *testScript) cmdStderr(want simpleStatus, args []string) { 844 scriptMatch(ts, want, args, ts.stderr, "stderr") 845} 846 847// grep checks that file content matches a regexp. 848// Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. 849func (ts *testScript) cmdGrep(want simpleStatus, args []string) { 850 scriptMatch(ts, want, args, "", "grep") 851} 852 853// scriptMatch implements both stdout and stderr. 854func scriptMatch(ts *testScript, want simpleStatus, args []string, text, name string) { 855 if want == successOrFailure { 856 ts.fatalf("unsupported: %v %s", want, name) 857 } 858 859 n := 0 860 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { 861 if want == failure { 862 ts.fatalf("cannot use -count= with negated match") 863 } 864 var err error 865 n, err = strconv.Atoi(args[0][len("-count="):]) 866 if err != nil { 867 ts.fatalf("bad -count=: %v", err) 868 } 869 if n < 1 { 870 ts.fatalf("bad -count=: must be at least 1") 871 } 872 args = args[1:] 873 } 874 quiet := false 875 if len(args) >= 1 && args[0] == "-q" { 876 quiet = true 877 args = args[1:] 878 } 879 880 extraUsage := "" 881 wantArgs := 1 882 if name == "grep" { 883 extraUsage = " file" 884 wantArgs = 2 885 } 886 if len(args) != wantArgs { 887 ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage) 888 } 889 890 pattern := `(?m)` + args[0] 891 re, err := regexp.Compile(pattern) 892 if err != nil { 893 ts.fatalf("regexp.Compile(%q): %v", pattern, err) 894 } 895 896 isGrep := name == "grep" 897 if isGrep { 898 name = args[1] // for error messages 899 data, err := os.ReadFile(ts.mkabs(args[1])) 900 ts.check(err) 901 text = string(data) 902 } 903 904 // Matching against workdir would be misleading. 905 text = strings.ReplaceAll(text, ts.workdir, "$WORK") 906 907 switch want { 908 case failure: 909 if re.MatchString(text) { 910 if isGrep && !quiet { 911 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 912 } 913 ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text)) 914 } 915 916 case success: 917 if !re.MatchString(text) { 918 if isGrep && !quiet { 919 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 920 } 921 ts.fatalf("no match for %#q found in %s", pattern, name) 922 } 923 if n > 0 { 924 count := len(re.FindAllString(text, -1)) 925 if count != n { 926 if isGrep && !quiet { 927 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) 928 } 929 ts.fatalf("have %d matches for %#q, want %d", count, pattern, n) 930 } 931 } 932 } 933} 934 935// stop stops execution of the test (marking it passed). 936func (ts *testScript) cmdStop(want simpleStatus, args []string) { 937 if want != success { 938 ts.fatalf("unsupported: %v stop", want) 939 } 940 if len(args) > 1 { 941 ts.fatalf("usage: stop [msg]") 942 } 943 if len(args) == 1 { 944 fmt.Fprintf(&ts.log, "stop: %s\n", args[0]) 945 } else { 946 fmt.Fprintf(&ts.log, "stop\n") 947 } 948 ts.stopped = true 949} 950 951// symlink creates a symbolic link. 952func (ts *testScript) cmdSymlink(want simpleStatus, args []string) { 953 if want != success { 954 ts.fatalf("unsupported: %v symlink", want) 955 } 956 if len(args) != 3 || args[1] != "->" { 957 ts.fatalf("usage: symlink file -> target") 958 } 959 // Note that the link target args[2] is not interpreted with mkabs: 960 // it will be interpreted relative to the directory file is in. 961 ts.check(os.Symlink(args[2], ts.mkabs(args[0]))) 962} 963 964// wait waits for background commands to exit, setting stderr and stdout to their result. 965func (ts *testScript) cmdWait(want simpleStatus, args []string) { 966 if want != success { 967 ts.fatalf("unsupported: %v wait", want) 968 } 969 if len(args) > 0 { 970 ts.fatalf("usage: wait") 971 } 972 973 var stdouts, stderrs []string 974 for _, bg := range ts.background { 975 <-bg.done 976 977 args := append([]string{filepath.Base(bg.args[0])}, bg.args[1:]...) 978 fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.err) 979 980 cmdStdout := bg.stdout.String() 981 if cmdStdout != "" { 982 fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout) 983 stdouts = append(stdouts, cmdStdout) 984 } 985 986 cmdStderr := bg.stderr.String() 987 if cmdStderr != "" { 988 fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr) 989 stderrs = append(stderrs, cmdStderr) 990 } 991 992 ts.checkCmd(bg) 993 } 994 995 ts.stdout = strings.Join(stdouts, "") 996 ts.stderr = strings.Join(stderrs, "") 997 ts.background = nil 998} 999 1000// Helpers for command implementations. 1001 1002// abbrev abbreviates the actual work directory in the string s to the literal string "$WORK". 1003func (ts *testScript) abbrev(s string) string { 1004 s = strings.ReplaceAll(s, ts.workdir, "$WORK") 1005 if *testWork { 1006 // Expose actual $WORK value in environment dump on first line of work script, 1007 // so that the user can find out what directory -testwork left behind. 1008 s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n") 1009 } 1010 return s 1011} 1012 1013// check calls ts.fatalf if err != nil. 1014func (ts *testScript) check(err error) { 1015 if err != nil { 1016 ts.fatalf("%v", err) 1017 } 1018} 1019 1020func (ts *testScript) checkCmd(bg *backgroundCmd) { 1021 select { 1022 case <-bg.done: 1023 default: 1024 panic("checkCmd called when not done") 1025 } 1026 1027 if bg.err == nil { 1028 if bg.want == failure { 1029 ts.fatalf("unexpected command success") 1030 } 1031 return 1032 } 1033 1034 if errors.Is(bg.err, context.DeadlineExceeded) { 1035 ts.fatalf("test timed out while running command") 1036 } 1037 1038 if errors.Is(bg.err, context.Canceled) { 1039 // The process was still running at the end of the test. 1040 // The test must not depend on its exit status. 1041 if bg.want != successOrFailure { 1042 ts.fatalf("unexpected background command remaining at test end") 1043 } 1044 return 1045 } 1046 1047 if bg.want == success { 1048 ts.fatalf("unexpected command failure") 1049 } 1050} 1051 1052// exec runs the given command line (an actual subprocess, not simulated) 1053// in ts.cd with environment ts.env and then returns collected standard output and standard error. 1054func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) { 1055 bg, err := ts.startBackground(success, command, args...) 1056 if err != nil { 1057 return "", "", err 1058 } 1059 <-bg.done 1060 return bg.stdout.String(), bg.stderr.String(), bg.err 1061} 1062 1063// startBackground starts the given command line (an actual subprocess, not simulated) 1064// in ts.cd with environment ts.env. 1065func (ts *testScript) startBackground(want simpleStatus, command string, args ...string) (*backgroundCmd, error) { 1066 done := make(chan struct{}) 1067 bg := &backgroundCmd{ 1068 want: want, 1069 args: append([]string{command}, args...), 1070 done: done, 1071 cancel: func() {}, 1072 } 1073 1074 ctx := context.Background() 1075 gracePeriod := 100 * time.Millisecond 1076 if deadline, ok := ts.t.Deadline(); ok { 1077 timeout := time.Until(deadline) 1078 // If time allows, increase the termination grace period to 5% of the 1079 // remaining time. 1080 if gp := timeout / 20; gp > gracePeriod { 1081 gracePeriod = gp 1082 } 1083 1084 // Send the first termination signal with two grace periods remaining. 1085 // If it still hasn't finished after the first period has elapsed, 1086 // we'll escalate to os.Kill with a second period remaining until the 1087 // test deadline.. 1088 timeout -= 2 * gracePeriod 1089 1090 if timeout <= 0 { 1091 // The test has less than the grace period remaining. There is no point in 1092 // even starting the command, because it will be terminated immediately. 1093 // Save the expense of starting it in the first place. 1094 bg.err = context.DeadlineExceeded 1095 close(done) 1096 return bg, nil 1097 } 1098 1099 ctx, bg.cancel = context.WithTimeout(ctx, timeout) 1100 } 1101 1102 cmd := exec.Command(command, args...) 1103 cmd.Dir = ts.cd 1104 cmd.Env = append(ts.env, "PWD="+ts.cd) 1105 cmd.Stdout = &bg.stdout 1106 cmd.Stderr = &bg.stderr 1107 if err := cmd.Start(); err != nil { 1108 bg.cancel() 1109 return nil, err 1110 } 1111 1112 go func() { 1113 bg.err = waitOrStop(ctx, cmd, stopSignal(), gracePeriod) 1114 close(done) 1115 }() 1116 return bg, nil 1117} 1118 1119// stopSignal returns the appropriate signal to use to request that a process 1120// stop execution. 1121func stopSignal() os.Signal { 1122 if runtime.GOOS == "windows" { 1123 // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on 1124 // Windows; using it with os.Process.Signal will return an error.” 1125 // Fall back to Kill instead. 1126 return os.Kill 1127 } 1128 return os.Interrupt 1129} 1130 1131// waitOrStop waits for the already-started command cmd by calling its Wait method. 1132// 1133// If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal. 1134// If killDelay is positive, waitOrStop waits that additional period for Wait to return before sending os.Kill. 1135// 1136// This function is copied from the one added to x/playground/internal in 1137// http://golang.org/cl/228438. 1138func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error { 1139 if cmd.Process == nil { 1140 panic("waitOrStop called with a nil cmd.Process — missing Start call?") 1141 } 1142 if interrupt == nil { 1143 panic("waitOrStop requires a non-nil interrupt signal") 1144 } 1145 1146 errc := make(chan error) 1147 go func() { 1148 select { 1149 case errc <- nil: 1150 return 1151 case <-ctx.Done(): 1152 } 1153 1154 err := cmd.Process.Signal(interrupt) 1155 if err == nil { 1156 err = ctx.Err() // Report ctx.Err() as the reason we interrupted. 1157 } else if err.Error() == "os: process already finished" { 1158 errc <- nil 1159 return 1160 } 1161 1162 if killDelay > 0 { 1163 timer := time.NewTimer(killDelay) 1164 select { 1165 // Report ctx.Err() as the reason we interrupted the process... 1166 case errc <- ctx.Err(): 1167 timer.Stop() 1168 return 1169 // ...but after killDelay has elapsed, fall back to a stronger signal. 1170 case <-timer.C: 1171 } 1172 1173 // Wait still hasn't returned. 1174 // Kill the process harder to make sure that it exits. 1175 // 1176 // Ignore any error: if cmd.Process has already terminated, we still 1177 // want to send ctx.Err() (or the error from the Interrupt call) 1178 // to properly attribute the signal that may have terminated it. 1179 _ = cmd.Process.Kill() 1180 } 1181 1182 errc <- err 1183 }() 1184 1185 waitErr := cmd.Wait() 1186 if interruptErr := <-errc; interruptErr != nil { 1187 return interruptErr 1188 } 1189 return waitErr 1190} 1191 1192// expand applies environment variable expansion to the string s. 1193func (ts *testScript) expand(s string, inRegexp bool) string { 1194 return os.Expand(s, func(key string) string { 1195 e := ts.envMap[key] 1196 if inRegexp { 1197 // Replace workdir with $WORK, since we have done the same substitution in 1198 // the text we're about to compare against. 1199 e = strings.ReplaceAll(e, ts.workdir, "$WORK") 1200 1201 // Quote to literal strings: we want paths like C:\work\go1.4 to remain 1202 // paths rather than regular expressions. 1203 e = regexp.QuoteMeta(e) 1204 } 1205 return e 1206 }) 1207} 1208 1209// fatalf aborts the test with the given failure message. 1210func (ts *testScript) fatalf(format string, args ...interface{}) { 1211 fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...)) 1212 ts.t.FailNow() 1213} 1214 1215// mkabs interprets file relative to the test script's current directory 1216// and returns the corresponding absolute path. 1217func (ts *testScript) mkabs(file string) string { 1218 if filepath.IsAbs(file) { 1219 return file 1220 } 1221 return filepath.Join(ts.cd, file) 1222} 1223 1224// A condition guards execution of a command. 1225type condition struct { 1226 want bool 1227 tag string 1228} 1229 1230// A command is a complete command parsed from a script. 1231type command struct { 1232 want simpleStatus 1233 conds []condition // all must be satisfied 1234 name string // the name of the command; must be non-empty 1235 args []string // shell-expanded arguments following name 1236} 1237 1238// parse parses a single line as a list of space-separated arguments 1239// subject to environment variable expansion (but not resplitting). 1240// Single quotes around text disable splitting and expansion. 1241// To embed a single quote, double it: 'Don''t communicate by sharing memory.' 1242func (ts *testScript) parse(line string) command { 1243 ts.line = line 1244 1245 var ( 1246 cmd command 1247 arg string // text of current arg so far (need to add line[start:i]) 1248 start = -1 // if >= 0, position where current arg text chunk starts 1249 quoted = false // currently processing quoted text 1250 isRegexp = false // currently processing unquoted regular expression 1251 ) 1252 1253 flushArg := func() { 1254 defer func() { 1255 arg = "" 1256 start = -1 1257 }() 1258 1259 if cmd.name != "" { 1260 cmd.args = append(cmd.args, arg) 1261 // Commands take only one regexp argument (after the optional flags), 1262 // so no subsequent args are regexps. Liberally assume an argument that 1263 // starts with a '-' is a flag. 1264 if len(arg) == 0 || arg[0] != '-' { 1265 isRegexp = false 1266 } 1267 return 1268 } 1269 1270 // Command prefix ! means negate the expectations about this command: 1271 // go command should fail, match should not be found, etc. 1272 // Prefix ? means allow either success or failure. 1273 switch want := simpleStatus(arg); want { 1274 case failure, successOrFailure: 1275 if cmd.want != "" { 1276 ts.fatalf("duplicated '!' or '?' token") 1277 } 1278 cmd.want = want 1279 return 1280 } 1281 1282 // Command prefix [cond] means only run this command if cond is satisfied. 1283 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") { 1284 want := true 1285 arg = strings.TrimSpace(arg[1 : len(arg)-1]) 1286 if strings.HasPrefix(arg, "!") { 1287 want = false 1288 arg = strings.TrimSpace(arg[1:]) 1289 } 1290 if arg == "" { 1291 ts.fatalf("empty condition") 1292 } 1293 cmd.conds = append(cmd.conds, condition{want: want, tag: arg}) 1294 return 1295 } 1296 1297 cmd.name = arg 1298 isRegexp = regexpCmd[cmd.name] 1299 } 1300 1301 for i := 0; ; i++ { 1302 if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') { 1303 // Found arg-separating space. 1304 if start >= 0 { 1305 arg += ts.expand(line[start:i], isRegexp) 1306 flushArg() 1307 } 1308 if i >= len(line) || line[i] == '#' { 1309 break 1310 } 1311 continue 1312 } 1313 if i >= len(line) { 1314 ts.fatalf("unterminated quoted argument") 1315 } 1316 if line[i] == '\'' { 1317 if !quoted { 1318 // starting a quoted chunk 1319 if start >= 0 { 1320 arg += ts.expand(line[start:i], isRegexp) 1321 } 1322 start = i + 1 1323 quoted = true 1324 continue 1325 } 1326 // 'foo''bar' means foo'bar, like in rc shell and Pascal. 1327 if i+1 < len(line) && line[i+1] == '\'' { 1328 arg += line[start:i] 1329 start = i + 1 1330 i++ // skip over second ' before next iteration 1331 continue 1332 } 1333 // ending a quoted chunk 1334 arg += line[start:i] 1335 start = i + 1 1336 quoted = false 1337 continue 1338 } 1339 // found character worth saving; make sure we're saving 1340 if start < 0 { 1341 start = i 1342 } 1343 } 1344 return cmd 1345} 1346 1347// diff returns a formatted diff of the two texts, 1348// showing the entire text and the minimum line-level 1349// additions and removals to turn text1 into text2. 1350// (That is, lines only in text1 appear with a leading -, 1351// and lines only in text2 appear with a leading +.) 1352func diff(text1, text2 string) string { 1353 if text1 != "" && !strings.HasSuffix(text1, "\n") { 1354 text1 += "(missing final newline)" 1355 } 1356 lines1 := strings.Split(text1, "\n") 1357 lines1 = lines1[:len(lines1)-1] // remove empty string after final line 1358 if text2 != "" && !strings.HasSuffix(text2, "\n") { 1359 text2 += "(missing final newline)" 1360 } 1361 lines2 := strings.Split(text2, "\n") 1362 lines2 = lines2[:len(lines2)-1] // remove empty string after final line 1363 1364 // Naive dynamic programming algorithm for edit distance. 1365 // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm 1366 // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] 1367 // (The reversed indices make following the minimum cost path 1368 // visit lines in the same order as in the text.) 1369 dist := make([][]int, len(lines1)+1) 1370 for i := range dist { 1371 dist[i] = make([]int, len(lines2)+1) 1372 if i == 0 { 1373 for j := range dist[0] { 1374 dist[0][j] = j 1375 } 1376 continue 1377 } 1378 for j := range dist[i] { 1379 if j == 0 { 1380 dist[i][0] = i 1381 continue 1382 } 1383 cost := dist[i][j-1] + 1 1384 if cost > dist[i-1][j]+1 { 1385 cost = dist[i-1][j] + 1 1386 } 1387 if lines1[len(lines1)-i] == lines2[len(lines2)-j] { 1388 if cost > dist[i-1][j-1] { 1389 cost = dist[i-1][j-1] 1390 } 1391 } 1392 dist[i][j] = cost 1393 } 1394 } 1395 1396 var buf strings.Builder 1397 i, j := len(lines1), len(lines2) 1398 for i > 0 || j > 0 { 1399 cost := dist[i][j] 1400 if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { 1401 fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) 1402 i-- 1403 j-- 1404 } else if i > 0 && cost == dist[i-1][j]+1 { 1405 fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) 1406 i-- 1407 } else { 1408 fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) 1409 j-- 1410 } 1411 } 1412 return buf.String() 1413} 1414 1415var diffTests = []struct { 1416 text1 string 1417 text2 string 1418 diff string 1419}{ 1420 {"a b c", "a b d e f", "a b -c +d +e +f"}, 1421 {"", "a b c", "+a +b +c"}, 1422 {"a b c", "", "-a -b -c"}, 1423 {"a b c", "d e f", "-a -b -c +d +e +f"}, 1424 {"a b c d e f", "a b d e f", "a b -c d e f"}, 1425 {"a b c e f", "a b c d e f", "a b c +d e f"}, 1426} 1427 1428func TestDiff(t *testing.T) { 1429 t.Parallel() 1430 1431 for _, tt := range diffTests { 1432 // Turn spaces into \n. 1433 text1 := strings.ReplaceAll(tt.text1, " ", "\n") 1434 if text1 != "" { 1435 text1 += "\n" 1436 } 1437 text2 := strings.ReplaceAll(tt.text2, " ", "\n") 1438 if text2 != "" { 1439 text2 += "\n" 1440 } 1441 out := diff(text1, text2) 1442 // Cut final \n, cut spaces, turn remaining \n into spaces. 1443 out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ") 1444 if out != tt.diff { 1445 t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) 1446 } 1447 } 1448} 1449