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