| // Copyright 2018 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Script-driven tests. |
| // See testdata/script/README for an overview. |
| |
| package main_test |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "go/build" |
| "internal/testenv" |
| "io/fs" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "cmd/go/internal/cfg" |
| "cmd/go/internal/imports" |
| "cmd/go/internal/par" |
| "cmd/go/internal/robustio" |
| "cmd/go/internal/txtar" |
| "cmd/go/internal/work" |
| "cmd/internal/objabi" |
| "cmd/internal/sys" |
| ) |
| |
| // TestScript runs the tests in testdata/script/*.txt. |
| func TestScript(t *testing.T) { |
| testenv.MustHaveGoBuild(t) |
| testenv.SkipIfShortAndSlow(t) |
| |
| files, err := filepath.Glob("testdata/script/*.txt") |
| if err != nil { |
| t.Fatal(err) |
| } |
| for _, file := range files { |
| file := file |
| name := strings.TrimSuffix(filepath.Base(file), ".txt") |
| t.Run(name, func(t *testing.T) { |
| t.Parallel() |
| ts := &testScript{t: t, name: name, file: file} |
| ts.setup() |
| if !*testWork { |
| defer removeAll(ts.workdir) |
| } |
| ts.run() |
| }) |
| } |
| } |
| |
| // A testScript holds execution state for a single test script. |
| type testScript struct { |
| t *testing.T |
| workdir string // temporary work dir ($WORK) |
| log bytes.Buffer // test execution log (printed at end of test) |
| mark int // offset of next log truncation |
| cd string // current directory during test execution; initially $WORK/gopath/src |
| name string // short name of test ("foo") |
| file string // full file name ("testdata/script/foo.txt") |
| lineno int // line number currently executing |
| line string // line currently executing |
| env []string // environment list (for os/exec) |
| envMap map[string]string // environment mapping (matches env) |
| stdout string // standard output from last 'go' command; for 'stdout' command |
| stderr string // standard error from last 'go' command; for 'stderr' command |
| stopped bool // test wants to stop early |
| start time.Time // time phase started |
| background []*backgroundCmd // backgrounded 'exec' and 'go' commands |
| } |
| |
| type backgroundCmd struct { |
| want simpleStatus |
| args []string |
| cancel context.CancelFunc |
| done <-chan struct{} |
| err error |
| stdout, stderr strings.Builder |
| } |
| |
| type simpleStatus string |
| |
| const ( |
| success simpleStatus = "" |
| failure simpleStatus = "!" |
| successOrFailure simpleStatus = "?" |
| ) |
| |
| var extraEnvKeys = []string{ |
| "SYSTEMROOT", // must be preserved on Windows to find DLLs; golang.org/issue/25210 |
| "WINDIR", // must be preserved on Windows to be able to run PowerShell command; golang.org/issue/30711 |
| "LD_LIBRARY_PATH", // must be preserved on Unix systems to find shared libraries |
| "CC", // don't lose user settings when invoking cgo |
| "GO_TESTING_GOTOOLS", // for gccgo testing |
| "GCCGO", // for gccgo testing |
| "GCCGOTOOLDIR", // for gccgo testing |
| } |
| |
| // setup sets up the test execution temporary directory and environment. |
| func (ts *testScript) setup() { |
| StartProxy() |
| ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name) |
| ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777)) |
| ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777)) |
| ts.cd = filepath.Join(ts.workdir, "gopath/src") |
| ts.env = []string{ |
| "WORK=" + ts.workdir, // must be first for ts.abbrev |
| "PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"), |
| homeEnvName() + "=/no-home", |
| "CCACHE_DISABLE=1", // ccache breaks with non-existent HOME |
| "GOARCH=" + runtime.GOARCH, |
| "GOCACHE=" + testGOCACHE, |
| "GODEBUG=" + os.Getenv("GODEBUG"), |
| "GOEXE=" + cfg.ExeSuffix, |
| "GOEXPSTRING=" + objabi.Expstring()[2:], |
| "GOOS=" + runtime.GOOS, |
| "GOPATH=" + filepath.Join(ts.workdir, "gopath"), |
| "GOPROXY=" + proxyURL, |
| "GOPRIVATE=", |
| "GOROOT=" + testGOROOT, |
| "GOROOT_FINAL=" + os.Getenv("GOROOT_FINAL"), // causes spurious rebuilds and breaks the "stale" built-in if not propagated |
| "TESTGO_GOROOT=" + testGOROOT, |
| "GOSUMDB=" + testSumDBVerifierKey, |
| "GONOPROXY=", |
| "GONOSUMDB=", |
| "GOVCS=*:all", |
| "PWD=" + ts.cd, |
| tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"), |
| "devnull=" + os.DevNull, |
| "goversion=" + goVersion(ts), |
| ":=" + string(os.PathListSeparator), |
| } |
| if !testenv.HasExternalNetwork() { |
| ts.env = append(ts.env, "TESTGONETWORK=panic", "TESTGOVCS=panic") |
| } |
| |
| if runtime.GOOS == "plan9" { |
| ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path")) |
| } |
| |
| for _, key := range extraEnvKeys { |
| if val := os.Getenv(key); val != "" { |
| ts.env = append(ts.env, key+"="+val) |
| } |
| } |
| |
| ts.envMap = make(map[string]string) |
| for _, kv := range ts.env { |
| if i := strings.Index(kv, "="); i >= 0 { |
| ts.envMap[kv[:i]] = kv[i+1:] |
| } |
| } |
| } |
| |
| // goVersion returns the current Go version. |
| func goVersion(ts *testScript) string { |
| tags := build.Default.ReleaseTags |
| version := tags[len(tags)-1] |
| if !regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`).MatchString(version) { |
| ts.fatalf("invalid go version %q", version) |
| } |
| return version[2:] |
| } |
| |
| var execCache par.Cache |
| |
| // run runs the test script. |
| func (ts *testScript) run() { |
| // Truncate log at end of last phase marker, |
| // discarding details of successful phase. |
| rewind := func() { |
| if !testing.Verbose() { |
| ts.log.Truncate(ts.mark) |
| } |
| } |
| |
| // Insert elapsed time for phase at end of phase marker |
| markTime := func() { |
| if ts.mark > 0 && !ts.start.IsZero() { |
| afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...) |
| ts.log.Truncate(ts.mark - 1) // cut \n and afterMark |
| fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds()) |
| ts.log.Write(afterMark) |
| } |
| ts.start = time.Time{} |
| } |
| |
| defer func() { |
| // On a normal exit from the test loop, background processes are cleaned up |
| // before we print PASS. If we return early (e.g., due to a test failure), |
| // don't print anything about the processes that were still running. |
| for _, bg := range ts.background { |
| bg.cancel() |
| } |
| for _, bg := range ts.background { |
| <-bg.done |
| } |
| ts.background = nil |
| |
| markTime() |
| // Flush testScript log to testing.T log. |
| ts.t.Log("\n" + ts.abbrev(ts.log.String())) |
| }() |
| |
| // Unpack archive. |
| a, err := txtar.ParseFile(ts.file) |
| ts.check(err) |
| for _, f := range a.Files { |
| name := ts.mkabs(ts.expand(f.Name, false)) |
| ts.check(os.MkdirAll(filepath.Dir(name), 0777)) |
| ts.check(os.WriteFile(name, f.Data, 0666)) |
| } |
| |
| // With -v or -testwork, start log with full environment. |
| if *testWork || testing.Verbose() { |
| // Display environment. |
| ts.cmdEnv(success, nil) |
| fmt.Fprintf(&ts.log, "\n") |
| ts.mark = ts.log.Len() |
| } |
| |
| // Run script. |
| // See testdata/script/README for documentation of script form. |
| script := string(a.Comment) |
| Script: |
| for script != "" { |
| // Extract next line. |
| ts.lineno++ |
| var line string |
| if i := strings.Index(script, "\n"); i >= 0 { |
| line, script = script[:i], script[i+1:] |
| } else { |
| line, script = script, "" |
| } |
| |
| // # is a comment indicating the start of new phase. |
| if strings.HasPrefix(line, "#") { |
| // If there was a previous phase, it succeeded, |
| // so rewind the log to delete its details (unless -v is in use). |
| // If nothing has happened at all since the mark, |
| // rewinding is a no-op and adding elapsed time |
| // for doing nothing is meaningless, so don't. |
| if ts.log.Len() > ts.mark { |
| rewind() |
| markTime() |
| } |
| // Print phase heading and mark start of phase output. |
| fmt.Fprintf(&ts.log, "%s\n", line) |
| ts.mark = ts.log.Len() |
| ts.start = time.Now() |
| continue |
| } |
| |
| // Parse input line. Ignore blanks entirely. |
| parsed := ts.parse(line) |
| if parsed.name == "" { |
| if parsed.want != "" || len(parsed.conds) > 0 { |
| ts.fatalf("missing command") |
| } |
| continue |
| } |
| |
| // Echo command to log. |
| fmt.Fprintf(&ts.log, "> %s\n", line) |
| |
| for _, cond := range parsed.conds { |
| // Known conds are: $GOOS, $GOARCH, runtime.Compiler, and 'short' (for testing.Short). |
| // |
| // NOTE: If you make changes here, update testdata/script/README too! |
| // |
| ok := false |
| switch cond.tag { |
| case runtime.GOOS, runtime.GOARCH, runtime.Compiler: |
| ok = true |
| case "short": |
| ok = testing.Short() |
| case "cgo": |
| ok = canCgo |
| case "msan": |
| ok = canMSan |
| case "race": |
| ok = canRace |
| case "net": |
| ok = testenv.HasExternalNetwork() |
| case "link": |
| ok = testenv.HasLink() |
| case "root": |
| ok = os.Geteuid() == 0 |
| case "symlink": |
| ok = testenv.HasSymlink() |
| case "case-sensitive": |
| ok = isCaseSensitive(ts.t) |
| default: |
| if strings.HasPrefix(cond.tag, "exec:") { |
| prog := cond.tag[len("exec:"):] |
| ok = execCache.Do(prog, func() interface{} { |
| if runtime.GOOS == "plan9" && prog == "git" { |
| // The Git command is usually not the real Git on Plan 9. |
| // See https://golang.org/issues/29640. |
| return false |
| } |
| _, err := exec.LookPath(prog) |
| return err == nil |
| }).(bool) |
| break |
| } |
| if strings.HasPrefix(cond.tag, "GODEBUG:") { |
| value := strings.TrimPrefix(cond.tag, "GODEBUG:") |
| parts := strings.Split(os.Getenv("GODEBUG"), ",") |
| for _, p := range parts { |
| if strings.TrimSpace(p) == value { |
| ok = true |
| break |
| } |
| } |
| break |
| } |
| if strings.HasPrefix(cond.tag, "buildmode:") { |
| value := strings.TrimPrefix(cond.tag, "buildmode:") |
| ok = sys.BuildModeSupported(runtime.Compiler, value, runtime.GOOS, runtime.GOARCH) |
| break |
| } |
| if !imports.KnownArch[cond.tag] && !imports.KnownOS[cond.tag] && cond.tag != "gc" && cond.tag != "gccgo" { |
| ts.fatalf("unknown condition %q", cond.tag) |
| } |
| } |
| if ok != cond.want { |
| // Don't run rest of line. |
| continue Script |
| } |
| } |
| |
| // Run command. |
| cmd := scriptCmds[parsed.name] |
| if cmd == nil { |
| ts.fatalf("unknown command %q", parsed.name) |
| } |
| cmd(ts, parsed.want, parsed.args) |
| |
| // Command can ask script to stop early. |
| if ts.stopped { |
| // Break instead of returning, so that we check the status of any |
| // background processes and print PASS. |
| break |
| } |
| } |
| |
| for _, bg := range ts.background { |
| bg.cancel() |
| } |
| ts.cmdWait(success, nil) |
| |
| // Final phase ended. |
| rewind() |
| markTime() |
| if !ts.stopped { |
| fmt.Fprintf(&ts.log, "PASS\n") |
| } |
| } |
| |
| var ( |
| onceCaseSensitive sync.Once |
| caseSensitive bool |
| ) |
| |
| func isCaseSensitive(t *testing.T) bool { |
| onceCaseSensitive.Do(func() { |
| tmpdir, err := os.MkdirTemp("", "case-sensitive") |
| if err != nil { |
| t.Fatal("failed to create directory to determine case-sensitivity:", err) |
| } |
| defer os.RemoveAll(tmpdir) |
| |
| fcap := filepath.Join(tmpdir, "FILE") |
| if err := os.WriteFile(fcap, []byte{}, 0644); err != nil { |
| t.Fatal("error writing file to determine case-sensitivity:", err) |
| } |
| |
| flow := filepath.Join(tmpdir, "file") |
| _, err = os.ReadFile(flow) |
| switch { |
| case err == nil: |
| caseSensitive = false |
| return |
| case os.IsNotExist(err): |
| caseSensitive = true |
| return |
| default: |
| t.Fatal("unexpected error reading file when determining case-sensitivity:", err) |
| } |
| }) |
| |
| return caseSensitive |
| } |
| |
| // scriptCmds are the script command implementations. |
| // Keep list and the implementations below sorted by name. |
| // |
| // NOTE: If you make changes here, update testdata/script/README too! |
| // |
| var scriptCmds = map[string]func(*testScript, simpleStatus, []string){ |
| "addcrlf": (*testScript).cmdAddcrlf, |
| "cc": (*testScript).cmdCc, |
| "cd": (*testScript).cmdCd, |
| "chmod": (*testScript).cmdChmod, |
| "cmp": (*testScript).cmdCmp, |
| "cmpenv": (*testScript).cmdCmpenv, |
| "cp": (*testScript).cmdCp, |
| "env": (*testScript).cmdEnv, |
| "exec": (*testScript).cmdExec, |
| "exists": (*testScript).cmdExists, |
| "go": (*testScript).cmdGo, |
| "grep": (*testScript).cmdGrep, |
| "mkdir": (*testScript).cmdMkdir, |
| "rm": (*testScript).cmdRm, |
| "skip": (*testScript).cmdSkip, |
| "stale": (*testScript).cmdStale, |
| "stderr": (*testScript).cmdStderr, |
| "stdout": (*testScript).cmdStdout, |
| "stop": (*testScript).cmdStop, |
| "symlink": (*testScript).cmdSymlink, |
| "wait": (*testScript).cmdWait, |
| } |
| |
| // When expanding shell variables for these commands, we apply regexp quoting to |
| // expanded strings within the first argument. |
| var regexpCmd = map[string]bool{ |
| "grep": true, |
| "stderr": true, |
| "stdout": true, |
| } |
| |
| // addcrlf adds CRLF line endings to the named files. |
| func (ts *testScript) cmdAddcrlf(want simpleStatus, args []string) { |
| if len(args) == 0 { |
| ts.fatalf("usage: addcrlf file...") |
| } |
| |
| for _, file := range args { |
| file = ts.mkabs(file) |
| data, err := os.ReadFile(file) |
| ts.check(err) |
| ts.check(os.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666)) |
| } |
| } |
| |
| // cc runs the C compiler along with platform specific options. |
| func (ts *testScript) cmdCc(want simpleStatus, args []string) { |
| if len(args) < 1 || (len(args) == 1 && args[0] == "&") { |
| ts.fatalf("usage: cc args... [&]") |
| } |
| |
| var b work.Builder |
| b.Init() |
| ts.cmdExec(want, append(b.GccCmd(".", ""), args...)) |
| robustio.RemoveAll(b.WorkDir) |
| } |
| |
| // cd changes to a different directory. |
| func (ts *testScript) cmdCd(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v cd", want) |
| } |
| if len(args) != 1 { |
| ts.fatalf("usage: cd dir") |
| } |
| |
| dir := args[0] |
| if !filepath.IsAbs(dir) { |
| dir = filepath.Join(ts.cd, dir) |
| } |
| info, err := os.Stat(dir) |
| if os.IsNotExist(err) { |
| ts.fatalf("directory %s does not exist", dir) |
| } |
| ts.check(err) |
| if !info.IsDir() { |
| ts.fatalf("%s is not a directory", dir) |
| } |
| ts.cd = dir |
| ts.envMap["PWD"] = dir |
| fmt.Fprintf(&ts.log, "%s\n", ts.cd) |
| } |
| |
| // chmod changes permissions for a file or directory. |
| func (ts *testScript) cmdChmod(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v chmod", want) |
| } |
| if len(args) < 2 { |
| ts.fatalf("usage: chmod perm paths...") |
| } |
| perm, err := strconv.ParseUint(args[0], 0, 32) |
| if err != nil || perm&uint64(fs.ModePerm) != perm { |
| ts.fatalf("invalid mode: %s", args[0]) |
| } |
| for _, arg := range args[1:] { |
| path := arg |
| if !filepath.IsAbs(path) { |
| path = filepath.Join(ts.cd, arg) |
| } |
| err := os.Chmod(path, fs.FileMode(perm)) |
| ts.check(err) |
| } |
| } |
| |
| // cmp compares two files. |
| func (ts *testScript) cmdCmp(want simpleStatus, args []string) { |
| if want != success { |
| // It would be strange to say "this file can have any content except this precise byte sequence". |
| ts.fatalf("unsupported: %v cmp", want) |
| } |
| quiet := false |
| if len(args) > 0 && args[0] == "-q" { |
| quiet = true |
| args = args[1:] |
| } |
| if len(args) != 2 { |
| ts.fatalf("usage: cmp file1 file2") |
| } |
| ts.doCmdCmp(args, false, quiet) |
| } |
| |
| // cmpenv compares two files with environment variable substitution. |
| func (ts *testScript) cmdCmpenv(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v cmpenv", want) |
| } |
| quiet := false |
| if len(args) > 0 && args[0] == "-q" { |
| quiet = true |
| args = args[1:] |
| } |
| if len(args) != 2 { |
| ts.fatalf("usage: cmpenv file1 file2") |
| } |
| ts.doCmdCmp(args, true, quiet) |
| } |
| |
| func (ts *testScript) doCmdCmp(args []string, env, quiet bool) { |
| name1, name2 := args[0], args[1] |
| var text1, text2 string |
| if name1 == "stdout" { |
| text1 = ts.stdout |
| } else if name1 == "stderr" { |
| text1 = ts.stderr |
| } else { |
| data, err := os.ReadFile(ts.mkabs(name1)) |
| ts.check(err) |
| text1 = string(data) |
| } |
| |
| data, err := os.ReadFile(ts.mkabs(name2)) |
| ts.check(err) |
| text2 = string(data) |
| |
| if env { |
| text1 = ts.expand(text1, false) |
| text2 = ts.expand(text2, false) |
| } |
| |
| if text1 == text2 { |
| return |
| } |
| |
| if !quiet { |
| fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2)) |
| } |
| ts.fatalf("%s and %s differ", name1, name2) |
| } |
| |
| // cp copies files, maybe eventually directories. |
| func (ts *testScript) cmdCp(want simpleStatus, args []string) { |
| if len(args) < 2 { |
| ts.fatalf("usage: cp src... dst") |
| } |
| |
| dst := ts.mkabs(args[len(args)-1]) |
| info, err := os.Stat(dst) |
| dstDir := err == nil && info.IsDir() |
| if len(args) > 2 && !dstDir { |
| ts.fatalf("cp: destination %s is not a directory", dst) |
| } |
| |
| for _, arg := range args[:len(args)-1] { |
| var ( |
| src string |
| data []byte |
| mode fs.FileMode |
| ) |
| switch arg { |
| case "stdout": |
| src = arg |
| data = []byte(ts.stdout) |
| mode = 0666 |
| case "stderr": |
| src = arg |
| data = []byte(ts.stderr) |
| mode = 0666 |
| default: |
| src = ts.mkabs(arg) |
| info, err := os.Stat(src) |
| ts.check(err) |
| mode = info.Mode() & 0777 |
| data, err = os.ReadFile(src) |
| ts.check(err) |
| } |
| targ := dst |
| if dstDir { |
| targ = filepath.Join(dst, filepath.Base(src)) |
| } |
| err := os.WriteFile(targ, data, mode) |
| switch want { |
| case failure: |
| if err == nil { |
| ts.fatalf("unexpected command success") |
| } |
| case success: |
| ts.check(err) |
| } |
| } |
| } |
| |
| // env displays or adds to the environment. |
| func (ts *testScript) cmdEnv(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v env", want) |
| } |
| |
| conv := func(s string) string { return s } |
| if len(args) > 0 && args[0] == "-r" { |
| conv = regexp.QuoteMeta |
| args = args[1:] |
| } |
| |
| var out strings.Builder |
| if len(args) == 0 { |
| printed := make(map[string]bool) // env list can have duplicates; only print effective value (from envMap) once |
| for _, kv := range ts.env { |
| k := kv[:strings.Index(kv, "=")] |
| if !printed[k] { |
| fmt.Fprintf(&out, "%s=%s\n", k, ts.envMap[k]) |
| } |
| } |
| } else { |
| for _, env := range args { |
| i := strings.Index(env, "=") |
| if i < 0 { |
| // Display value instead of setting it. |
| fmt.Fprintf(&out, "%s=%s\n", env, ts.envMap[env]) |
| continue |
| } |
| key, val := env[:i], conv(env[i+1:]) |
| ts.env = append(ts.env, key+"="+val) |
| ts.envMap[key] = val |
| } |
| } |
| if out.Len() > 0 || len(args) > 0 { |
| ts.stdout = out.String() |
| ts.log.WriteString(out.String()) |
| } |
| } |
| |
| // exec runs the given command. |
| func (ts *testScript) cmdExec(want simpleStatus, args []string) { |
| if len(args) < 1 || (len(args) == 1 && args[0] == "&") { |
| ts.fatalf("usage: exec program [args...] [&]") |
| } |
| |
| background := false |
| if len(args) > 0 && args[len(args)-1] == "&" { |
| background = true |
| args = args[:len(args)-1] |
| } |
| |
| bg, err := ts.startBackground(want, args[0], args[1:]...) |
| if err != nil { |
| ts.fatalf("unexpected error starting command: %v", err) |
| } |
| if background { |
| ts.stdout, ts.stderr = "", "" |
| ts.background = append(ts.background, bg) |
| return |
| } |
| |
| <-bg.done |
| ts.stdout = bg.stdout.String() |
| ts.stderr = bg.stderr.String() |
| if ts.stdout != "" { |
| fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout) |
| } |
| if ts.stderr != "" { |
| fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr) |
| } |
| if bg.err != nil { |
| fmt.Fprintf(&ts.log, "[%v]\n", bg.err) |
| } |
| ts.checkCmd(bg) |
| } |
| |
| // exists checks that the list of files exists. |
| func (ts *testScript) cmdExists(want simpleStatus, args []string) { |
| if want == successOrFailure { |
| ts.fatalf("unsupported: %v exists", want) |
| } |
| var readonly, exec bool |
| loop: |
| for len(args) > 0 { |
| switch args[0] { |
| case "-readonly": |
| readonly = true |
| args = args[1:] |
| case "-exec": |
| exec = true |
| args = args[1:] |
| default: |
| break loop |
| } |
| } |
| if len(args) == 0 { |
| ts.fatalf("usage: exists [-readonly] [-exec] file...") |
| } |
| |
| for _, file := range args { |
| file = ts.mkabs(file) |
| info, err := os.Stat(file) |
| if err == nil && want == failure { |
| what := "file" |
| if info.IsDir() { |
| what = "directory" |
| } |
| ts.fatalf("%s %s unexpectedly exists", what, file) |
| } |
| if err != nil && want == success { |
| ts.fatalf("%s does not exist", file) |
| } |
| if err == nil && want == success && readonly && info.Mode()&0222 != 0 { |
| ts.fatalf("%s exists but is writable", file) |
| } |
| if err == nil && want == success && exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 { |
| ts.fatalf("%s exists but is not executable", file) |
| } |
| } |
| } |
| |
| // go runs the go command. |
| func (ts *testScript) cmdGo(want simpleStatus, args []string) { |
| ts.cmdExec(want, append([]string{testGo}, args...)) |
| } |
| |
| // mkdir creates directories. |
| func (ts *testScript) cmdMkdir(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v mkdir", want) |
| } |
| if len(args) < 1 { |
| ts.fatalf("usage: mkdir dir...") |
| } |
| for _, arg := range args { |
| ts.check(os.MkdirAll(ts.mkabs(arg), 0777)) |
| } |
| } |
| |
| // rm removes files or directories. |
| func (ts *testScript) cmdRm(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v rm", want) |
| } |
| if len(args) < 1 { |
| ts.fatalf("usage: rm file...") |
| } |
| for _, arg := range args { |
| file := ts.mkabs(arg) |
| removeAll(file) // does chmod and then attempts rm |
| ts.check(robustio.RemoveAll(file)) // report error |
| } |
| } |
| |
| // skip marks the test skipped. |
| func (ts *testScript) cmdSkip(want simpleStatus, args []string) { |
| if len(args) > 1 { |
| ts.fatalf("usage: skip [msg]") |
| } |
| if want != success { |
| ts.fatalf("unsupported: %v skip", want) |
| } |
| |
| // Before we mark the test as skipped, shut down any background processes and |
| // make sure they have returned the correct status. |
| for _, bg := range ts.background { |
| bg.cancel() |
| } |
| ts.cmdWait(success, nil) |
| |
| if len(args) == 1 { |
| ts.t.Skip(args[0]) |
| } |
| ts.t.Skip() |
| } |
| |
| // stale checks that the named build targets are stale. |
| func (ts *testScript) cmdStale(want simpleStatus, args []string) { |
| if len(args) == 0 { |
| ts.fatalf("usage: stale target...") |
| } |
| tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{{else}}" |
| switch want { |
| case failure: |
| tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale{{end}}" |
| case success: |
| tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}" |
| default: |
| ts.fatalf("unsupported: %v stale", want) |
| } |
| tmpl += "{{end}}" |
| goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...) |
| stdout, stderr, err := ts.exec(testGo, goArgs...) |
| if err != nil { |
| ts.fatalf("go list: %v\n%s%s", err, stdout, stderr) |
| } |
| if stdout != "" { |
| ts.fatalf("%s", stdout) |
| } |
| } |
| |
| // stdout checks that the last go command standard output matches a regexp. |
| func (ts *testScript) cmdStdout(want simpleStatus, args []string) { |
| scriptMatch(ts, want, args, ts.stdout, "stdout") |
| } |
| |
| // stderr checks that the last go command standard output matches a regexp. |
| func (ts *testScript) cmdStderr(want simpleStatus, args []string) { |
| scriptMatch(ts, want, args, ts.stderr, "stderr") |
| } |
| |
| // grep checks that file content matches a regexp. |
| // Like stdout/stderr and unlike Unix grep, it accepts Go regexp syntax. |
| func (ts *testScript) cmdGrep(want simpleStatus, args []string) { |
| scriptMatch(ts, want, args, "", "grep") |
| } |
| |
| // scriptMatch implements both stdout and stderr. |
| func scriptMatch(ts *testScript, want simpleStatus, args []string, text, name string) { |
| if want == successOrFailure { |
| ts.fatalf("unsupported: %v %s", want, name) |
| } |
| |
| n := 0 |
| if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") { |
| if want == failure { |
| ts.fatalf("cannot use -count= with negated match") |
| } |
| var err error |
| n, err = strconv.Atoi(args[0][len("-count="):]) |
| if err != nil { |
| ts.fatalf("bad -count=: %v", err) |
| } |
| if n < 1 { |
| ts.fatalf("bad -count=: must be at least 1") |
| } |
| args = args[1:] |
| } |
| quiet := false |
| if len(args) >= 1 && args[0] == "-q" { |
| quiet = true |
| args = args[1:] |
| } |
| |
| extraUsage := "" |
| wantArgs := 1 |
| if name == "grep" { |
| extraUsage = " file" |
| wantArgs = 2 |
| } |
| if len(args) != wantArgs { |
| ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage) |
| } |
| |
| pattern := `(?m)` + args[0] |
| re, err := regexp.Compile(pattern) |
| if err != nil { |
| ts.fatalf("regexp.Compile(%q): %v", pattern, err) |
| } |
| |
| isGrep := name == "grep" |
| if isGrep { |
| name = args[1] // for error messages |
| data, err := os.ReadFile(ts.mkabs(args[1])) |
| ts.check(err) |
| text = string(data) |
| } |
| |
| // Matching against workdir would be misleading. |
| text = strings.ReplaceAll(text, ts.workdir, "$WORK") |
| |
| switch want { |
| case failure: |
| if re.MatchString(text) { |
| if isGrep && !quiet { |
| fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) |
| } |
| ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text)) |
| } |
| |
| case success: |
| if !re.MatchString(text) { |
| if isGrep && !quiet { |
| fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) |
| } |
| ts.fatalf("no match for %#q found in %s", pattern, name) |
| } |
| if n > 0 { |
| count := len(re.FindAllString(text, -1)) |
| if count != n { |
| if isGrep && !quiet { |
| fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text) |
| } |
| ts.fatalf("have %d matches for %#q, want %d", count, pattern, n) |
| } |
| } |
| } |
| } |
| |
| // stop stops execution of the test (marking it passed). |
| func (ts *testScript) cmdStop(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v stop", want) |
| } |
| if len(args) > 1 { |
| ts.fatalf("usage: stop [msg]") |
| } |
| if len(args) == 1 { |
| fmt.Fprintf(&ts.log, "stop: %s\n", args[0]) |
| } else { |
| fmt.Fprintf(&ts.log, "stop\n") |
| } |
| ts.stopped = true |
| } |
| |
| // symlink creates a symbolic link. |
| func (ts *testScript) cmdSymlink(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v symlink", want) |
| } |
| if len(args) != 3 || args[1] != "->" { |
| ts.fatalf("usage: symlink file -> target") |
| } |
| // Note that the link target args[2] is not interpreted with mkabs: |
| // it will be interpreted relative to the directory file is in. |
| ts.check(os.Symlink(args[2], ts.mkabs(args[0]))) |
| } |
| |
| // wait waits for background commands to exit, setting stderr and stdout to their result. |
| func (ts *testScript) cmdWait(want simpleStatus, args []string) { |
| if want != success { |
| ts.fatalf("unsupported: %v wait", want) |
| } |
| if len(args) > 0 { |
| ts.fatalf("usage: wait") |
| } |
| |
| var stdouts, stderrs []string |
| for _, bg := range ts.background { |
| <-bg.done |
| |
| args := append([]string{filepath.Base(bg.args[0])}, bg.args[1:]...) |
| fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.err) |
| |
| cmdStdout := bg.stdout.String() |
| if cmdStdout != "" { |
| fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout) |
| stdouts = append(stdouts, cmdStdout) |
| } |
| |
| cmdStderr := bg.stderr.String() |
| if cmdStderr != "" { |
| fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr) |
| stderrs = append(stderrs, cmdStderr) |
| } |
| |
| ts.checkCmd(bg) |
| } |
| |
| ts.stdout = strings.Join(stdouts, "") |
| ts.stderr = strings.Join(stderrs, "") |
| ts.background = nil |
| } |
| |
| // Helpers for command implementations. |
| |
| // abbrev abbreviates the actual work directory in the string s to the literal string "$WORK". |
| func (ts *testScript) abbrev(s string) string { |
| s = strings.ReplaceAll(s, ts.workdir, "$WORK") |
| if *testWork { |
| // Expose actual $WORK value in environment dump on first line of work script, |
| // so that the user can find out what directory -testwork left behind. |
| s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n") |
| } |
| return s |
| } |
| |
| // check calls ts.fatalf if err != nil. |
| func (ts *testScript) check(err error) { |
| if err != nil { |
| ts.fatalf("%v", err) |
| } |
| } |
| |
| func (ts *testScript) checkCmd(bg *backgroundCmd) { |
| select { |
| case <-bg.done: |
| default: |
| panic("checkCmd called when not done") |
| } |
| |
| if bg.err == nil { |
| if bg.want == failure { |
| ts.fatalf("unexpected command success") |
| } |
| return |
| } |
| |
| if errors.Is(bg.err, context.DeadlineExceeded) { |
| ts.fatalf("test timed out while running command") |
| } |
| |
| if errors.Is(bg.err, context.Canceled) { |
| // The process was still running at the end of the test. |
| // The test must not depend on its exit status. |
| if bg.want != successOrFailure { |
| ts.fatalf("unexpected background command remaining at test end") |
| } |
| return |
| } |
| |
| if bg.want == success { |
| ts.fatalf("unexpected command failure") |
| } |
| } |
| |
| // exec runs the given command line (an actual subprocess, not simulated) |
| // in ts.cd with environment ts.env and then returns collected standard output and standard error. |
| func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) { |
| bg, err := ts.startBackground(success, command, args...) |
| if err != nil { |
| return "", "", err |
| } |
| <-bg.done |
| return bg.stdout.String(), bg.stderr.String(), bg.err |
| } |
| |
| // startBackground starts the given command line (an actual subprocess, not simulated) |
| // in ts.cd with environment ts.env. |
| func (ts *testScript) startBackground(want simpleStatus, command string, args ...string) (*backgroundCmd, error) { |
| done := make(chan struct{}) |
| bg := &backgroundCmd{ |
| want: want, |
| args: append([]string{command}, args...), |
| done: done, |
| cancel: func() {}, |
| } |
| |
| ctx := context.Background() |
| gracePeriod := 100 * time.Millisecond |
| if deadline, ok := ts.t.Deadline(); ok { |
| timeout := time.Until(deadline) |
| // If time allows, increase the termination grace period to 5% of the |
| // remaining time. |
| if gp := timeout / 20; gp > gracePeriod { |
| gracePeriod = gp |
| } |
| |
| // Send the first termination signal with two grace periods remaining. |
| // If it still hasn't finished after the first period has elapsed, |
| // we'll escalate to os.Kill with a second period remaining until the |
| // test deadline.. |
| timeout -= 2 * gracePeriod |
| |
| if timeout <= 0 { |
| // The test has less than the grace period remaining. There is no point in |
| // even starting the command, because it will be terminated immediately. |
| // Save the expense of starting it in the first place. |
| bg.err = context.DeadlineExceeded |
| close(done) |
| return bg, nil |
| } |
| |
| ctx, bg.cancel = context.WithTimeout(ctx, timeout) |
| } |
| |
| cmd := exec.Command(command, args...) |
| cmd.Dir = ts.cd |
| cmd.Env = append(ts.env, "PWD="+ts.cd) |
| cmd.Stdout = &bg.stdout |
| cmd.Stderr = &bg.stderr |
| if err := cmd.Start(); err != nil { |
| bg.cancel() |
| return nil, err |
| } |
| |
| go func() { |
| bg.err = waitOrStop(ctx, cmd, stopSignal(), gracePeriod) |
| close(done) |
| }() |
| return bg, nil |
| } |
| |
| // stopSignal returns the appropriate signal to use to request that a process |
| // stop execution. |
| func stopSignal() os.Signal { |
| if runtime.GOOS == "windows" { |
| // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on |
| // Windows; using it with os.Process.Signal will return an error.” |
| // Fall back to Kill instead. |
| return os.Kill |
| } |
| return os.Interrupt |
| } |
| |
| // waitOrStop waits for the already-started command cmd by calling its Wait method. |
| // |
| // If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal. |
| // If killDelay is positive, waitOrStop waits that additional period for Wait to return before sending os.Kill. |
| // |
| // This function is copied from the one added to x/playground/internal in |
| // http://golang.org/cl/228438. |
| func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error { |
| if cmd.Process == nil { |
| panic("waitOrStop called with a nil cmd.Process — missing Start call?") |
| } |
| if interrupt == nil { |
| panic("waitOrStop requires a non-nil interrupt signal") |
| } |
| |
| errc := make(chan error) |
| go func() { |
| select { |
| case errc <- nil: |
| return |
| case <-ctx.Done(): |
| } |
| |
| err := cmd.Process.Signal(interrupt) |
| if err == nil { |
| err = ctx.Err() // Report ctx.Err() as the reason we interrupted. |
| } else if err.Error() == "os: process already finished" { |
| errc <- nil |
| return |
| } |
| |
| if killDelay > 0 { |
| timer := time.NewTimer(killDelay) |
| select { |
| // Report ctx.Err() as the reason we interrupted the process... |
| case errc <- ctx.Err(): |
| timer.Stop() |
| return |
| // ...but after killDelay has elapsed, fall back to a stronger signal. |
| case <-timer.C: |
| } |
| |
| // Wait still hasn't returned. |
| // Kill the process harder to make sure that it exits. |
| // |
| // Ignore any error: if cmd.Process has already terminated, we still |
| // want to send ctx.Err() (or the error from the Interrupt call) |
| // to properly attribute the signal that may have terminated it. |
| _ = cmd.Process.Kill() |
| } |
| |
| errc <- err |
| }() |
| |
| waitErr := cmd.Wait() |
| if interruptErr := <-errc; interruptErr != nil { |
| return interruptErr |
| } |
| return waitErr |
| } |
| |
| // expand applies environment variable expansion to the string s. |
| func (ts *testScript) expand(s string, inRegexp bool) string { |
| return os.Expand(s, func(key string) string { |
| e := ts.envMap[key] |
| if inRegexp { |
| // Replace workdir with $WORK, since we have done the same substitution in |
| // the text we're about to compare against. |
| e = strings.ReplaceAll(e, ts.workdir, "$WORK") |
| |
| // Quote to literal strings: we want paths like C:\work\go1.4 to remain |
| // paths rather than regular expressions. |
| e = regexp.QuoteMeta(e) |
| } |
| return e |
| }) |
| } |
| |
| // fatalf aborts the test with the given failure message. |
| func (ts *testScript) fatalf(format string, args ...interface{}) { |
| fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...)) |
| ts.t.FailNow() |
| } |
| |
| // mkabs interprets file relative to the test script's current directory |
| // and returns the corresponding absolute path. |
| func (ts *testScript) mkabs(file string) string { |
| if filepath.IsAbs(file) { |
| return file |
| } |
| return filepath.Join(ts.cd, file) |
| } |
| |
| // A condition guards execution of a command. |
| type condition struct { |
| want bool |
| tag string |
| } |
| |
| // A command is a complete command parsed from a script. |
| type command struct { |
| want simpleStatus |
| conds []condition // all must be satisfied |
| name string // the name of the command; must be non-empty |
| args []string // shell-expanded arguments following name |
| } |
| |
| // parse parses a single line as a list of space-separated arguments |
| // subject to environment variable expansion (but not resplitting). |
| // Single quotes around text disable splitting and expansion. |
| // To embed a single quote, double it: 'Don''t communicate by sharing memory.' |
| func (ts *testScript) parse(line string) command { |
| ts.line = line |
| |
| var ( |
| cmd command |
| arg string // text of current arg so far (need to add line[start:i]) |
| start = -1 // if >= 0, position where current arg text chunk starts |
| quoted = false // currently processing quoted text |
| isRegexp = false // currently processing unquoted regular expression |
| ) |
| |
| flushArg := func() { |
| defer func() { |
| arg = "" |
| start = -1 |
| }() |
| |
| if cmd.name != "" { |
| cmd.args = append(cmd.args, arg) |
| // Commands take only one regexp argument (after the optional flags), |
| // so no subsequent args are regexps. Liberally assume an argument that |
| // starts with a '-' is a flag. |
| if len(arg) == 0 || arg[0] != '-' { |
| isRegexp = false |
| } |
| return |
| } |
| |
| // Command prefix ! means negate the expectations about this command: |
| // go command should fail, match should not be found, etc. |
| // Prefix ? means allow either success or failure. |
| switch want := simpleStatus(arg); want { |
| case failure, successOrFailure: |
| if cmd.want != "" { |
| ts.fatalf("duplicated '!' or '?' token") |
| } |
| cmd.want = want |
| return |
| } |
| |
| // Command prefix [cond] means only run this command if cond is satisfied. |
| if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") { |
| want := true |
| arg = strings.TrimSpace(arg[1 : len(arg)-1]) |
| if strings.HasPrefix(arg, "!") { |
| want = false |
| arg = strings.TrimSpace(arg[1:]) |
| } |
| if arg == "" { |
| ts.fatalf("empty condition") |
| } |
| cmd.conds = append(cmd.conds, condition{want: want, tag: arg}) |
| return |
| } |
| |
| cmd.name = arg |
| isRegexp = regexpCmd[cmd.name] |
| } |
| |
| for i := 0; ; i++ { |
| if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') { |
| // Found arg-separating space. |
| if start >= 0 { |
| arg += ts.expand(line[start:i], isRegexp) |
| flushArg() |
| } |
| if i >= len(line) || line[i] == '#' { |
| break |
| } |
| continue |
| } |
| if i >= len(line) { |
| ts.fatalf("unterminated quoted argument") |
| } |
| if line[i] == '\'' { |
| if !quoted { |
| // starting a quoted chunk |
| if start >= 0 { |
| arg += ts.expand(line[start:i], isRegexp) |
| } |
| start = i + 1 |
| quoted = true |
| continue |
| } |
| // 'foo''bar' means foo'bar, like in rc shell and Pascal. |
| if i+1 < len(line) && line[i+1] == '\'' { |
| arg += line[start:i] |
| start = i + 1 |
| i++ // skip over second ' before next iteration |
| continue |
| } |
| // ending a quoted chunk |
| arg += line[start:i] |
| start = i + 1 |
| quoted = false |
| continue |
| } |
| // found character worth saving; make sure we're saving |
| if start < 0 { |
| start = i |
| } |
| } |
| return cmd |
| } |
| |
| // diff returns a formatted diff of the two texts, |
| // showing the entire text and the minimum line-level |
| // additions and removals to turn text1 into text2. |
| // (That is, lines only in text1 appear with a leading -, |
| // and lines only in text2 appear with a leading +.) |
| func diff(text1, text2 string) string { |
| if text1 != "" && !strings.HasSuffix(text1, "\n") { |
| text1 += "(missing final newline)" |
| } |
| lines1 := strings.Split(text1, "\n") |
| lines1 = lines1[:len(lines1)-1] // remove empty string after final line |
| if text2 != "" && !strings.HasSuffix(text2, "\n") { |
| text2 += "(missing final newline)" |
| } |
| lines2 := strings.Split(text2, "\n") |
| lines2 = lines2[:len(lines2)-1] // remove empty string after final line |
| |
| // Naive dynamic programming algorithm for edit distance. |
| // https://en.wikipedia.org/wiki/Wagner–Fischer_algorithm |
| // dist[i][j] = edit distance between lines1[:len(lines1)-i] and lines2[:len(lines2)-j] |
| // (The reversed indices make following the minimum cost path |
| // visit lines in the same order as in the text.) |
| dist := make([][]int, len(lines1)+1) |
| for i := range dist { |
| dist[i] = make([]int, len(lines2)+1) |
| if i == 0 { |
| for j := range dist[0] { |
| dist[0][j] = j |
| } |
| continue |
| } |
| for j := range dist[i] { |
| if j == 0 { |
| dist[i][0] = i |
| continue |
| } |
| cost := dist[i][j-1] + 1 |
| if cost > dist[i-1][j]+1 { |
| cost = dist[i-1][j] + 1 |
| } |
| if lines1[len(lines1)-i] == lines2[len(lines2)-j] { |
| if cost > dist[i-1][j-1] { |
| cost = dist[i-1][j-1] |
| } |
| } |
| dist[i][j] = cost |
| } |
| } |
| |
| var buf strings.Builder |
| i, j := len(lines1), len(lines2) |
| for i > 0 || j > 0 { |
| cost := dist[i][j] |
| if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] { |
| fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i]) |
| i-- |
| j-- |
| } else if i > 0 && cost == dist[i-1][j]+1 { |
| fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i]) |
| i-- |
| } else { |
| fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j]) |
| j-- |
| } |
| } |
| return buf.String() |
| } |
| |
| var diffTests = []struct { |
| text1 string |
| text2 string |
| diff string |
| }{ |
| {"a b c", "a b d e f", "a b -c +d +e +f"}, |
| {"", "a b c", "+a +b +c"}, |
| {"a b c", "", "-a -b -c"}, |
| {"a b c", "d e f", "-a -b -c +d +e +f"}, |
| {"a b c d e f", "a b d e f", "a b -c d e f"}, |
| {"a b c e f", "a b c d e f", "a b c +d e f"}, |
| } |
| |
| func TestDiff(t *testing.T) { |
| t.Parallel() |
| |
| for _, tt := range diffTests { |
| // Turn spaces into \n. |
| text1 := strings.ReplaceAll(tt.text1, " ", "\n") |
| if text1 != "" { |
| text1 += "\n" |
| } |
| text2 := strings.ReplaceAll(tt.text2, " ", "\n") |
| if text2 != "" { |
| text2 += "\n" |
| } |
| out := diff(text1, text2) |
| // Cut final \n, cut spaces, turn remaining \n into spaces. |
| out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ") |
| if out != tt.diff { |
| t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff) |
| } |
| } |
| } |