// 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 := (skipError{}); errors.As(err, &skip) { 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]} }) } 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( " names an executable in the test binary's PATH", func(name string) (bool, error) { _, err := pathcache.LookPath(name) return err == nil, nil }) }