| // Copyright 2022 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. | |
| // Package scripttest adapts the script engine for use in tests. | |
| package scripttest | |
| import ( | |
| "bufio" | |
| "cmd/internal/pathcache" | |
| "cmd/internal/script" | |
| "errors" | |
| "io" | |
| "strings" | |
| "testing" | |
| ) | |
| // DefaultCmds returns a set of broadly useful script commands. | |
| // | |
| // This set includes all of the commands in script.DefaultCmds, | |
| // as well as a "skip" command that halts the script and causes the | |
| // testing.TB passed to Run to be skipped. | |
| func DefaultCmds() map[string]script.Cmd { | |
| cmds := script.DefaultCmds() | |
| cmds["skip"] = Skip() | |
| return cmds | |
| } | |
| // DefaultConds returns a set of broadly useful script conditions. | |
| // | |
| // This set includes all of the conditions in script.DefaultConds, | |
| // as well as: | |
| // | |
| // - Conditions of the form "exec:foo" are active when the executable "foo" is | |
| // found in the test process's PATH, and inactive when the executable is | |
| // not found. | |
| // | |
| // - "short" is active when testing.Short() is true. | |
| // | |
| // - "verbose" is active when testing.Verbose() is true. | |
| func DefaultConds() map[string]script.Cond { | |
| conds := script.DefaultConds() | |
| conds["exec"] = CachedExec() | |
| conds["short"] = script.BoolCondition("testing.Short()", testing.Short()) | |
| conds["verbose"] = script.BoolCondition("testing.Verbose()", testing.Verbose()) | |
| return conds | |
| } | |
| // Run runs the script from the given filename starting at the given initial state. | |
| // When the script completes, Run closes the state. | |
| func Run(t testing.TB, e *script.Engine, s *script.State, filename string, testScript io.Reader) { | |
| t.Helper() | |
| err := func() (err error) { | |
| log := new(strings.Builder) | |
| log.WriteString("\n") // Start output on a new line for consistent indentation. | |
| // Defer writing to the test log in case the script engine panics during execution, | |
| // but write the log before we write the final "skip" or "FAIL" line. | |
| t.Helper() | |
| defer func() { | |
| t.Helper() | |
| if closeErr := s.CloseAndWait(log); err == nil { | |
| err = closeErr | |
| } | |
| if log.Len() > 0 { | |
| t.Log(strings.TrimSuffix(log.String(), "\n")) | |
| } | |
| }() | |
| if testing.Verbose() { | |
| // Add the environment to the start of the script log. | |
| wait, err := script.Env().Run(s) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if wait != nil { | |
| stdout, stderr, err := wait(s) | |
| if err != nil { | |
| t.Fatalf("env: %v\n%s", err, stderr) | |
| } | |
| if len(stdout) > 0 { | |
| s.Logf("%s\n", stdout) | |
| } | |
| } | |
| } | |
| return e.Execute(s, filename, bufio.NewReader(testScript), log) | |
| }() | |
| if skip, ok := errors.AsType[SkipError](err); ok { | |
| if skip.msg == "" { | |
| t.Skip("SKIP") | |
| } else { | |
| t.Skipf("SKIP: %v", skip.msg) | |
| } | |
| } | |
| if err != nil { | |
| t.Errorf("FAIL: %v", err) | |
| } | |
| } | |
| // Skip returns a sentinel error that causes Run to mark the test as skipped. | |
| func Skip() script.Cmd { | |
| return script.Command( | |
| script.CmdUsage{ | |
| Summary: "skip the current test", | |
| Args: "[msg]", | |
| }, | |
| func(_ *script.State, args ...string) (script.WaitFunc, error) { | |
| if len(args) > 1 { | |
| return nil, script.ErrUsage | |
| } | |
| if len(args) == 0 { | |
| return nil, SkipError{""} | |
| } | |
| return nil, SkipError{args[0]} | |
| }) | |
| } | |
| // SkipError is returned by a script test that executes the [Skip] command. | |
| type SkipError struct { | |
| msg string | |
| } | |
| func (s SkipError) Error() string { | |
| if s.msg == "" { | |
| return "skip" | |
| } | |
| return s.msg | |
| } | |
| // CachedExec returns a Condition that reports whether the PATH of the test | |
| // binary itself (not the script's current environment) contains the named | |
| // executable. | |
| func CachedExec() script.Cond { | |
| return script.CachedCondition( | |
| "<suffix> names an executable in the test binary's PATH", | |
| func(name string) (bool, error) { | |
| _, err := pathcache.LookPath(name) | |
| return err == nil, nil | |
| }) | |
| } |