1// Copyright 2022 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// Package scripttest adapts the script engine for use in tests.
6package scripttest
7
8import (
9	"bufio"
10	"cmd/go/internal/cfg"
11	"cmd/go/internal/script"
12	"errors"
13	"io"
14	"strings"
15	"testing"
16)
17
18// DefaultCmds returns a set of broadly useful script commands.
19//
20// This set includes all of the commands in script.DefaultCmds,
21// as well as a "skip" command that halts the script and causes the
22// testing.TB passed to Run to be skipped.
23func DefaultCmds() map[string]script.Cmd {
24	cmds := script.DefaultCmds()
25	cmds["skip"] = Skip()
26	return cmds
27}
28
29// DefaultConds returns a set of broadly useful script conditions.
30//
31// This set includes all of the conditions in script.DefaultConds,
32// as well as:
33//
34//   - Conditions of the form "exec:foo" are active when the executable "foo" is
35//     found in the test process's PATH, and inactive when the executable is
36//     not found.
37//
38//   - "short" is active when testing.Short() is true.
39//
40//   - "verbose" is active when testing.Verbose() is true.
41func DefaultConds() map[string]script.Cond {
42	conds := script.DefaultConds()
43	conds["exec"] = CachedExec()
44	conds["short"] = script.BoolCondition("testing.Short()", testing.Short())
45	conds["verbose"] = script.BoolCondition("testing.Verbose()", testing.Verbose())
46	return conds
47}
48
49// Run runs the script from the given filename starting at the given initial state.
50// When the script completes, Run closes the state.
51func Run(t testing.TB, e *script.Engine, s *script.State, filename string, testScript io.Reader) {
52	t.Helper()
53	err := func() (err error) {
54		log := new(strings.Builder)
55		log.WriteString("\n") // Start output on a new line for consistent indentation.
56
57		// Defer writing to the test log in case the script engine panics during execution,
58		// but write the log before we write the final "skip" or "FAIL" line.
59		t.Helper()
60		defer func() {
61			t.Helper()
62
63			if closeErr := s.CloseAndWait(log); err == nil {
64				err = closeErr
65			}
66
67			if log.Len() > 0 {
68				t.Log(strings.TrimSuffix(log.String(), "\n"))
69			}
70		}()
71
72		if testing.Verbose() {
73			// Add the environment to the start of the script log.
74			wait, err := script.Env().Run(s)
75			if err != nil {
76				t.Fatal(err)
77			}
78			if wait != nil {
79				stdout, stderr, err := wait(s)
80				if err != nil {
81					t.Fatalf("env: %v\n%s", err, stderr)
82				}
83				if len(stdout) > 0 {
84					s.Logf("%s\n", stdout)
85				}
86			}
87		}
88
89		return e.Execute(s, filename, bufio.NewReader(testScript), log)
90	}()
91
92	if skip := (skipError{}); errors.As(err, &skip) {
93		if skip.msg == "" {
94			t.Skip("SKIP")
95		} else {
96			t.Skipf("SKIP: %v", skip.msg)
97		}
98	}
99	if err != nil {
100		t.Errorf("FAIL: %v", err)
101	}
102}
103
104// Skip returns a sentinel error that causes Run to mark the test as skipped.
105func Skip() script.Cmd {
106	return script.Command(
107		script.CmdUsage{
108			Summary: "skip the current test",
109			Args:    "[msg]",
110		},
111		func(_ *script.State, args ...string) (script.WaitFunc, error) {
112			if len(args) > 1 {
113				return nil, script.ErrUsage
114			}
115			if len(args) == 0 {
116				return nil, skipError{""}
117			}
118			return nil, skipError{args[0]}
119		})
120}
121
122type skipError struct {
123	msg string
124}
125
126func (s skipError) Error() string {
127	if s.msg == "" {
128		return "skip"
129	}
130	return s.msg
131}
132
133// CachedExec returns a Condition that reports whether the PATH of the test
134// binary itself (not the script's current environment) contains the named
135// executable.
136func CachedExec() script.Cond {
137	return script.CachedCondition(
138		"<suffix> names an executable in the test binary's PATH",
139		func(name string) (bool, error) {
140			_, err := cfg.LookPath(name)
141			return err == nil, nil
142		})
143}
144