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 interrupt := func(cmd *exec.Cmd) error {
107
108
109 return cmd.Process.Signal(os.Interrupt)
110 }
111
112
113
114 gracePeriod := subprocessGracePeriod(t.Deadline())
115
116 cmdExec := script.Exec(interrupt, gracePeriod)
117 cmds["exec"] = cmdExec
118
119
120
121
122 goroot := goEnv("GOROOT")
123 tmpdir := t.TempDir()
124 tgr := SetupTestGoRoot(t, tmpdir, goroot)
125
126
127 for _, repl := range repls {
128 ReplaceGoToolInTestGoRoot(t, tgr, repl.ToolName, repl.ReplacementPath)
129 }
130
131
132 testgo := filepath.Join(tgr, "bin", "go")
133 gocmd := script.Program(testgo, interrupt, gracePeriod)
134 addcmd("go", gocmd)
135 addcmd("cc", scriptCC(cmdExec, goEnv("CC")))
136
137
138 goHostOS, goHostArch := goEnv("GOHOSTOS"), goEnv("GOHOSTARCH")
139 AddToolChainScriptConditions(t, conds, goHostOS, goHostArch)
140
141
142 env := os.Environ()
143 prependToPath(env, filepath.Join(tgr, "bin"))
144 env = setenv(env, "GOROOT", tgr)
145
146 env = setenv(env, "GOOS", runtime.GOOS)
147 env = setenv(env, "GOARCH", runtime.GOARCH)
148 for _, repl := range repls {
149
150 chunks := strings.Split(repl.EnvVar, "=")
151 if len(chunks) != 2 {
152 t.Fatalf("malformed env var setting: %s", repl.EnvVar)
153 }
154 env = append(env, repl.EnvVar)
155 }
156
157
158 engine := &script.Engine{
159 Conds: conds,
160 Cmds: cmds,
161 Quiet: !testing.Verbose(),
162 }
163
164 return engine, env
165 }
166
167
168
169
170
171
172 func RunToolScriptTest(t *testing.T, repls []ToolReplacement, scriptsdir string, fixReadme bool) {
173
174 gotool, err := testenv.GoTool()
175 if err != nil {
176 t.Fatalf("locating go tool: %v", err)
177 }
178
179 engine, env := NewEngine(t, repls)
180
181 t.Run("README", func(t *testing.T) {
182 checkScriptReadme(t, engine, env, scriptsdir, gotool, fixReadme)
183 })
184
185
186 ctx := context.Background()
187 pattern := filepath.Join(scriptsdir, "*.txt")
188 RunTests(t, ctx, engine, env, pattern)
189 }
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204 func ScriptTestContext(t *testing.T, ctx context.Context) context.Context {
205 deadline, ok := t.Deadline()
206 if !ok {
207 return ctx
208 }
209
210 gracePeriod := subprocessGracePeriod(deadline, ok)
211
212
213 timeout := time.Until(deadline)
214 timeout -= 2 * gracePeriod
215
216 ctx, cancel := context.WithTimeout(ctx, timeout)
217 t.Cleanup(cancel)
218 return ctx
219 }
220
221
222
223 func subprocessGracePeriod(deadline time.Time, hasDeadline bool) time.Duration {
224 gracePeriod := 100 * time.Millisecond
225 if !hasDeadline {
226 return gracePeriod
227 }
228
229
230
231 timeout := time.Until(deadline)
232 return max(gracePeriod, timeout/20)
233 }
234
235
236
237
238
239 func RunTests(t *testing.T, ctx context.Context, engine *script.Engine, env []string, pattern string) {
240 ctx = ScriptTestContext(t, ctx)
241
242 files, _ := filepath.Glob(pattern)
243 if len(files) == 0 {
244 t.Fatal("no testdata")
245 }
246 for _, file := range files {
247 file := file
248 name := strings.TrimSuffix(filepath.Base(file), ".txt")
249 t.Run(name, func(t *testing.T) {
250 t.Parallel()
251
252 workdir := t.TempDir()
253 s, err := script.NewState(ctx, workdir, env)
254 if err != nil {
255 t.Fatal(err)
256 }
257
258
259
260
261 defer fixPermissions(t, workdir)
262
263
264 a, err := txtar.ParseFile(file)
265 if err != nil {
266 t.Fatal(err)
267 }
268 InitScriptDirs(t, s)
269 if err := s.ExtractFiles(a); err != nil {
270 t.Fatal(err)
271 }
272
273 t.Log(time.Now().UTC().Format(time.RFC3339))
274 work, _ := s.LookupEnv("WORK")
275 t.Logf("$WORK=%s", work)
276
277
278
279
280
281
282 Run(t, engine, s, file, bytes.NewReader(a.Comment))
283 })
284 }
285 }
286
287 func fixPermissions(t *testing.T, dir string) {
288 t.Helper()
289
290
291
292 filepath.WalkDir(dir, func(path string, info fs.DirEntry, err error) error {
293
294
295 if err != nil || info.IsDir() {
296 os.Chmod(path, 0777)
297 }
298 return nil
299 })
300 }
301
302
303
304
305
306
307 func InitScriptDirs(t testing.TB, s *script.State) {
308 must := func(err error) {
309 if err != nil {
310 t.Helper()
311 t.Fatal(err)
312 }
313 }
314
315 work := s.Getwd()
316 must(s.Setenv("WORK", work))
317 must(os.MkdirAll(filepath.Join(work, "tmp"), 0777))
318 must(s.Setenv(tempEnvName(), filepath.Join(work, "tmp")))
319 }
320
321 func tempEnvName() string {
322 switch runtime.GOOS {
323 case "windows":
324 return "TMP"
325 case "plan9":
326 return "TMPDIR"
327 default:
328 return "TMPDIR"
329 }
330 }
331
332
333 func scriptCC(cmdExec script.Cmd, ccexe string) script.Cmd {
334 return script.Command(
335 script.CmdUsage{
336 Summary: "run the platform C compiler",
337 Args: "args...",
338 },
339 func(s *script.State, args ...string) (script.WaitFunc, error) {
340 return cmdExec.Run(s, append([]string{ccexe}, args...)...)
341 })
342 }
343
View as plain text