1
2
3
4
5
6 package scripttest
7
8 import (
9 "bytes"
10 "cmd/internal/script"
11 "context"
12 "fmt"
13 "internal/testenv"
14 "internal/txtar"
15 "io/fs"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "runtime"
20 "strings"
21 "testing"
22 "time"
23 )
24
25
26
27 type ToolReplacement struct {
28 ToolName string
29 ReplacementPath string
30 EnvVar string
31 }
32
33
34
35 func NewEngine(t *testing.T, repls []ToolReplacement) (*script.Engine, []string) {
36
37
38 testenv.MustHaveGoBuild(t)
39
40
41
42 if runtime.GOOS == "plan9" {
43 t.Skipf("no symlinks on plan9")
44 }
45
46
47 gotool, err := testenv.GoTool()
48 if err != nil {
49 t.Fatalf("locating go tool: %v", err)
50 }
51
52 goEnv := func(name string) string {
53 out, err := exec.Command(gotool, "env", name).CombinedOutput()
54 if err != nil {
55 t.Fatalf("go env %s: %v\n%s", name, err, out)
56 }
57 return strings.TrimSpace(string(out))
58 }
59
60
61
62 cmds := DefaultCmds()
63 conds := DefaultConds()
64
65 addcmd := func(name string, cmd script.Cmd) {
66 if _, ok := cmds[name]; ok {
67 panic(fmt.Sprintf("command %q is already registered", name))
68 }
69 cmds[name] = cmd
70 }
71
72 prependToPath := func(env []string, dir string) {
73 found := false
74 for k := range env {
75 ev := env[k]
76 if !strings.HasPrefix(ev, "PATH=") {
77 continue
78 }
79 oldpath := ev[5:]
80 env[k] = "PATH=" + dir + string(filepath.ListSeparator) + oldpath
81 found = true
82 break
83 }
84 if !found {
85 t.Fatalf("could not update PATH")
86 }
87 }
88
89 setenv := func(env []string, varname, val string) []string {
90 pref := varname + "="
91 found := false
92 for k := range env {
93 if !strings.HasPrefix(env[k], pref) {
94 continue
95 }
96 env[k] = pref + val
97 found = true
98 break
99 }
100 if !found {
101 env = append(env, varname+"="+val)
102 }
103 return env
104 }
105
106
107
108 gracePeriod := subprocessGracePeriod(t.Deadline())
109
110 cmdExec := script.Exec(script.InterruptCmd, gracePeriod)
111 cmds["exec"] = cmdExec
112
113
114
115
116 goroot := goEnv("GOROOT")
117 tmpdir := t.TempDir()
118 tgr := SetupTestGoRoot(t, tmpdir, goroot)
119
120
121 for _, repl := range repls {
122 ReplaceGoToolInTestGoRoot(t, tgr, repl.ToolName, repl.ReplacementPath)
123 }
124
125
126 testgo := filepath.Join(tgr, "bin", "go")
127 gocmd := script.Program(testgo, script.InterruptCmd, gracePeriod)
128 addcmd("go", gocmd)
129 addcmd("cc", scriptCC(cmdExec, goEnv("CC")))
130
131
132 goHostOS, goHostArch := goEnv("GOHOSTOS"), goEnv("GOHOSTARCH")
133 AddToolChainScriptConditions(t, conds, goHostOS, goHostArch)
134
135
136 env := os.Environ()
137 prependToPath(env, filepath.Join(tgr, "bin"))
138 env = setenv(env, "GOROOT", tgr)
139
140 env = setenv(env, "GOOS", runtime.GOOS)
141 env = setenv(env, "GOARCH", runtime.GOARCH)
142 for _, repl := range repls {
143
144 chunks := strings.Split(repl.EnvVar, "=")
145 if len(chunks) != 2 {
146 t.Fatalf("malformed env var setting: %s", repl.EnvVar)
147 }
148 env = append(env, repl.EnvVar)
149 }
150
151
152 engine := &script.Engine{
153 Conds: conds,
154 Cmds: cmds,
155 Quiet: !testing.Verbose(),
156 }
157
158 return engine, env
159 }
160
161
162
163
164
165
166 func RunToolScriptTest(t *testing.T, repls []ToolReplacement, scriptsdir string, fixReadme bool) {
167
168 gotool, err := testenv.GoTool()
169 if err != nil {
170 t.Fatalf("locating go tool: %v", err)
171 }
172
173 engine, env := NewEngine(t, repls)
174
175 t.Run("README", func(t *testing.T) {
176 checkScriptReadme(t, engine, env, scriptsdir, gotool, fixReadme)
177 })
178
179
180 ctx := context.Background()
181 pattern := filepath.Join(scriptsdir, "*.txt")
182 RunTests(t, ctx, engine, env, pattern)
183 }
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198 func ScriptTestContext(t *testing.T, ctx context.Context) context.Context {
199 deadline, ok := t.Deadline()
200 if !ok {
201 return ctx
202 }
203
204 gracePeriod := subprocessGracePeriod(deadline, ok)
205
206
207 timeout := time.Until(deadline)
208 timeout -= 2 * gracePeriod
209
210 ctx, cancel := context.WithTimeout(ctx, timeout)
211 t.Cleanup(cancel)
212 return ctx
213 }
214
215
216
217 func subprocessGracePeriod(deadline time.Time, hasDeadline bool) time.Duration {
218 gracePeriod := 100 * time.Millisecond
219 if !hasDeadline {
220 return gracePeriod
221 }
222
223
224
225 timeout := time.Until(deadline)
226 return max(gracePeriod, timeout/20)
227 }
228
229
230
231
232
233 func RunTests(t *testing.T, ctx context.Context, engine *script.Engine, env []string, pattern string) {
234 ctx = ScriptTestContext(t, ctx)
235
236 files, _ := filepath.Glob(pattern)
237 if len(files) == 0 {
238 t.Fatal("no testdata")
239 }
240 for _, file := range files {
241 file := file
242 name := strings.TrimSuffix(filepath.Base(file), ".txt")
243 t.Run(name, func(t *testing.T) {
244 t.Parallel()
245
246 workdir := t.TempDir()
247 s, err := script.NewState(ctx, workdir, env)
248 if err != nil {
249 t.Fatal(err)
250 }
251
252
253
254
255 defer fixPermissions(t, workdir)
256
257
258 a, err := txtar.ParseFile(file)
259 if err != nil {
260 t.Fatal(err)
261 }
262 InitScriptDirs(t, s)
263 if err := s.ExtractFiles(a); err != nil {
264 t.Fatal(err)
265 }
266
267 t.Log(time.Now().UTC().Format(time.RFC3339))
268 work, _ := s.LookupEnv("WORK")
269 t.Logf("$WORK=%s", work)
270
271
272
273
274
275
276 Run(t, engine, s, file, bytes.NewReader(a.Comment))
277 })
278 }
279 }
280
281 func fixPermissions(t *testing.T, dir string) {
282 t.Helper()
283
284
285
286 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
287
288
289 if err != nil || info.IsDir() {
290 os.Chmod(path, 0777)
291 }
292 return nil
293 })
294 }
295
296
297
298
299
300
301 func InitScriptDirs(t testing.TB, s *script.State) {
302 must := func(err error) {
303 if err != nil {
304 t.Helper()
305 t.Fatal(err)
306 }
307 }
308
309 work := s.Getwd()
310 must(s.Setenv("WORK", work))
311 must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
312 must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
313 }
314
315 func tempEnvName() string {
316 switch runtime.GOOS {
317 case "windows":
318 return "TMP"
319 case "plan9":
320 return "TMPDIR"
321 default:
322 return "TMPDIR"
323 }
324 }
325
326
327 func scriptCC(cmdExec script.Cmd, ccexe string) script.Cmd {
328 return script.Command(
329 script.CmdUsage{
330 Summary: "run the platform C compiler",
331 Args: "args...",
332 },
333 func(s *script.State, args ...string) (script.WaitFunc, error) {
334 return cmdExec.Run(s, append([]string{ccexe}, args...)...)
335 })
336 }
337
View as plain text