1
2
3
4
5 package testscript
6
7 import (
8 "bufio"
9 "bytes"
10 "fmt"
11 "io/ioutil"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "regexp"
16 "runtime"
17 "strconv"
18 "strings"
19
20 "github.com/rogpeppe/go-internal/diff"
21 "github.com/rogpeppe/go-internal/testscript/internal/pty"
22 "github.com/rogpeppe/go-internal/txtar"
23 )
24
25
26
27
28
29 var scriptCmds = map[string]func(*TestScript, bool, []string){
30 "cd": (*TestScript).cmdCd,
31 "chmod": (*TestScript).cmdChmod,
32 "cmp": (*TestScript).cmdCmp,
33 "cmpenv": (*TestScript).cmdCmpenv,
34 "cp": (*TestScript).cmdCp,
35 "env": (*TestScript).cmdEnv,
36 "exec": (*TestScript).cmdExec,
37 "exists": (*TestScript).cmdExists,
38 "grep": (*TestScript).cmdGrep,
39 "mkdir": (*TestScript).cmdMkdir,
40 "mv": (*TestScript).cmdMv,
41 "rm": (*TestScript).cmdRm,
42 "skip": (*TestScript).cmdSkip,
43 "stderr": (*TestScript).cmdStderr,
44 "stdin": (*TestScript).cmdStdin,
45 "stdout": (*TestScript).cmdStdout,
46 "ttyin": (*TestScript).cmdTtyin,
47 "ttyout": (*TestScript).cmdTtyout,
48 "stop": (*TestScript).cmdStop,
49 "symlink": (*TestScript).cmdSymlink,
50 "unix2dos": (*TestScript).cmdUNIX2DOS,
51 "unquote": (*TestScript).cmdUnquote,
52 "wait": (*TestScript).cmdWait,
53 }
54
55
56 func (ts *TestScript) cmdCd(neg bool, args []string) {
57 if neg {
58 ts.Fatalf("unsupported: ! cd")
59 }
60 if len(args) != 1 {
61 ts.Fatalf("usage: cd dir")
62 }
63
64 dir := args[0]
65 if !filepath.IsAbs(dir) {
66 dir = filepath.Join(ts.cd, dir)
67 }
68 info, err := os.Stat(dir)
69 if os.IsNotExist(err) {
70 ts.Fatalf("directory %s does not exist", dir)
71 }
72 ts.Check(err)
73 if !info.IsDir() {
74 ts.Fatalf("%s is not a directory", dir)
75 }
76 ts.cd = dir
77 ts.Logf("%s\n", ts.cd)
78 }
79
80 func (ts *TestScript) cmdChmod(neg bool, args []string) {
81 if neg {
82 ts.Fatalf("unsupported: ! chmod")
83 }
84 if len(args) != 2 {
85 ts.Fatalf("usage: chmod perm paths...")
86 }
87 perm, err := strconv.ParseUint(args[0], 8, 32)
88 if err != nil || perm&uint64(os.ModePerm) != perm {
89 ts.Fatalf("invalid mode: %s", args[0])
90 }
91 for _, arg := range args[1:] {
92 path := arg
93 if !filepath.IsAbs(path) {
94 path = filepath.Join(ts.cd, arg)
95 }
96 err := os.Chmod(path, os.FileMode(perm))
97 ts.Check(err)
98 }
99 }
100
101
102 func (ts *TestScript) cmdCmp(neg bool, args []string) {
103 if len(args) != 2 {
104 ts.Fatalf("usage: cmp file1 file2")
105 }
106
107 ts.doCmdCmp(neg, args, false)
108 }
109
110
111 func (ts *TestScript) cmdCmpenv(neg bool, args []string) {
112 if len(args) != 2 {
113 ts.Fatalf("usage: cmpenv file1 file2")
114 }
115 ts.doCmdCmp(neg, args, true)
116 }
117
118 func (ts *TestScript) doCmdCmp(neg bool, args []string, env bool) {
119 name1, name2 := args[0], args[1]
120 text1 := ts.ReadFile(name1)
121
122 absName2 := ts.MkAbs(name2)
123 data, err := ioutil.ReadFile(absName2)
124 ts.Check(err)
125 text2 := string(data)
126 if env {
127 text2 = ts.expand(text2)
128 }
129 eq := text1 == text2
130 if neg {
131 if eq {
132 ts.Fatalf("%s and %s do not differ", name1, name2)
133 }
134 return
135 }
136 if eq {
137 return
138 }
139 if ts.params.UpdateScripts && !env {
140 if scriptFile, ok := ts.scriptFiles[absName2]; ok {
141 ts.scriptUpdates[scriptFile] = text1
142 return
143 }
144
145
146 }
147
148 unifiedDiff := diff.Diff(name1, []byte(text1), name2, []byte(text2))
149
150 ts.Logf("%s", unifiedDiff)
151 ts.Fatalf("%s and %s differ", name1, name2)
152 }
153
154
155 func (ts *TestScript) cmdCp(neg bool, args []string) {
156 if neg {
157 ts.Fatalf("unsupported: ! cp")
158 }
159 if len(args) < 2 {
160 ts.Fatalf("usage: cp src... dst")
161 }
162
163 dst := ts.MkAbs(args[len(args)-1])
164 info, err := os.Stat(dst)
165 dstDir := err == nil && info.IsDir()
166 if len(args) > 2 && !dstDir {
167 ts.Fatalf("cp: destination %s is not a directory", dst)
168 }
169
170 for _, arg := range args[:len(args)-1] {
171 var (
172 src string
173 data []byte
174 mode os.FileMode
175 )
176 switch arg {
177 case "stdout":
178 src = arg
179 data = []byte(ts.stdout)
180 mode = 0o666
181 case "stderr":
182 src = arg
183 data = []byte(ts.stderr)
184 mode = 0o666
185 case "ttyout":
186 src = arg
187 data = []byte(ts.ttyout)
188 mode = 0o666
189 default:
190 src = ts.MkAbs(arg)
191 info, err := os.Stat(src)
192 ts.Check(err)
193 mode = info.Mode() & 0o777
194 data, err = ioutil.ReadFile(src)
195 ts.Check(err)
196 }
197 targ := dst
198 if dstDir {
199 targ = filepath.Join(dst, filepath.Base(src))
200 }
201 ts.Check(ioutil.WriteFile(targ, data, mode))
202 }
203 }
204
205
206 func (ts *TestScript) cmdEnv(neg bool, args []string) {
207 if neg {
208 ts.Fatalf("unsupported: ! env")
209 }
210 if len(args) == 0 {
211 printed := make(map[string]bool)
212 for _, kv := range ts.env {
213 k := envvarname(kv[:strings.Index(kv, "=")])
214 if !printed[k] {
215 printed[k] = true
216 ts.Logf("%s=%s\n", k, ts.envMap[k])
217 }
218 }
219 return
220 }
221 for _, env := range args {
222 i := strings.Index(env, "=")
223 if i < 0 {
224
225 ts.Logf("%s=%s\n", env, ts.Getenv(env))
226 continue
227 }
228 ts.Setenv(env[:i], env[i+1:])
229 }
230 }
231
232 var backgroundSpecifier = regexp.MustCompile(`^&([a-zA-Z_0-9]+&)?$`)
233
234
235 func (ts *TestScript) cmdExec(neg bool, args []string) {
236 if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
237 ts.Fatalf("usage: exec program [args...] [&]")
238 }
239
240 var err error
241 if len(args) > 0 && backgroundSpecifier.MatchString(args[len(args)-1]) {
242 bgName := strings.TrimSuffix(strings.TrimPrefix(args[len(args)-1], "&"), "&")
243 if ts.findBackground(bgName) != nil {
244 ts.Fatalf("duplicate background process name %q", bgName)
245 }
246 var cmd *exec.Cmd
247 cmd, err = ts.execBackground(args[0], args[1:len(args)-1]...)
248 if err == nil {
249 wait := make(chan struct{})
250 go func() {
251 waitOrStop(ts.ctxt, cmd, -1)
252 close(wait)
253 }()
254 ts.background = append(ts.background, backgroundCmd{bgName, cmd, wait, neg})
255 }
256 ts.stdout, ts.stderr = "", ""
257 } else {
258 ts.stdout, ts.stderr, err = ts.exec(args[0], args[1:]...)
259 if ts.stdout != "" {
260 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
261 }
262 if ts.stderr != "" {
263 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
264 }
265 if err == nil && neg {
266 ts.Fatalf("unexpected command success")
267 }
268 }
269
270 if err != nil {
271 fmt.Fprintf(&ts.log, "[%v]\n", err)
272 if ts.ctxt.Err() != nil {
273 ts.Fatalf("test timed out while running command")
274 } else if !neg {
275 ts.Fatalf("unexpected command failure")
276 }
277 }
278 }
279
280
281 func (ts *TestScript) cmdExists(neg bool, args []string) {
282 var readonly bool
283 if len(args) > 0 && args[0] == "-readonly" {
284 readonly = true
285 args = args[1:]
286 }
287 if len(args) == 0 {
288 ts.Fatalf("usage: exists [-readonly] file...")
289 }
290
291 for _, file := range args {
292 file = ts.MkAbs(file)
293 info, err := os.Stat(file)
294 if err == nil && neg {
295 what := "file"
296 if info.IsDir() {
297 what = "directory"
298 }
299 ts.Fatalf("%s %s unexpectedly exists", what, file)
300 }
301 if err != nil && !neg {
302 ts.Fatalf("%s does not exist", file)
303 }
304 if err == nil && !neg && readonly && info.Mode()&0o222 != 0 {
305 ts.Fatalf("%s exists but is writable", file)
306 }
307 }
308 }
309
310
311 func (ts *TestScript) cmdMkdir(neg bool, args []string) {
312 if neg {
313 ts.Fatalf("unsupported: ! mkdir")
314 }
315 if len(args) < 1 {
316 ts.Fatalf("usage: mkdir dir...")
317 }
318 for _, arg := range args {
319 ts.Check(os.MkdirAll(ts.MkAbs(arg), 0o777))
320 }
321 }
322
323 func (ts *TestScript) cmdMv(neg bool, args []string) {
324 if neg {
325 ts.Fatalf("unsupported: ! mv")
326 }
327 if len(args) != 2 {
328 ts.Fatalf("usage: mv old new")
329 }
330 ts.Check(os.Rename(ts.MkAbs(args[0]), ts.MkAbs(args[1])))
331 }
332
333
334 func (ts *TestScript) cmdUnquote(neg bool, args []string) {
335 if neg {
336 ts.Fatalf("unsupported: ! unquote")
337 }
338 for _, arg := range args {
339 file := ts.MkAbs(arg)
340 data, err := ioutil.ReadFile(file)
341 ts.Check(err)
342 data, err = txtar.Unquote(data)
343 ts.Check(err)
344 err = ioutil.WriteFile(file, data, 0o666)
345 ts.Check(err)
346 }
347 }
348
349
350 func (ts *TestScript) cmdRm(neg bool, args []string) {
351 if neg {
352 ts.Fatalf("unsupported: ! rm")
353 }
354 if len(args) < 1 {
355 ts.Fatalf("usage: rm file...")
356 }
357 for _, arg := range args {
358 file := ts.MkAbs(arg)
359 removeAll(file)
360 ts.Check(os.RemoveAll(file))
361 }
362 }
363
364
365 func (ts *TestScript) cmdSkip(neg bool, args []string) {
366 if len(args) > 1 {
367 ts.Fatalf("usage: skip [msg]")
368 }
369 if neg {
370 ts.Fatalf("unsupported: ! skip")
371 }
372
373
374
375 for _, bg := range ts.background {
376 interruptProcess(bg.cmd.Process)
377 }
378 ts.cmdWait(false, nil)
379
380 if len(args) == 1 {
381 ts.t.Skip(args[0])
382 }
383 ts.t.Skip()
384 }
385
386 func (ts *TestScript) cmdStdin(neg bool, args []string) {
387 if neg {
388 ts.Fatalf("unsupported: ! stdin")
389 }
390 if len(args) != 1 {
391 ts.Fatalf("usage: stdin filename")
392 }
393 if ts.stdinPty {
394 ts.Fatalf("conflicting use of 'stdin' and 'ttyin -stdin'")
395 }
396 ts.stdin = ts.ReadFile(args[0])
397 }
398
399
400 func (ts *TestScript) cmdStdout(neg bool, args []string) {
401 scriptMatch(ts, neg, args, ts.stdout, "stdout")
402 }
403
404
405 func (ts *TestScript) cmdStderr(neg bool, args []string) {
406 scriptMatch(ts, neg, args, ts.stderr, "stderr")
407 }
408
409
410
411 func (ts *TestScript) cmdGrep(neg bool, args []string) {
412 scriptMatch(ts, neg, args, "", "grep")
413 }
414
415 func (ts *TestScript) cmdTtyin(neg bool, args []string) {
416 if !pty.Supported {
417 ts.Fatalf("unsupported: ttyin on %s", runtime.GOOS)
418 }
419 if neg {
420 ts.Fatalf("unsupported: ! ttyin")
421 }
422 switch len(args) {
423 case 1:
424 ts.ttyin = ts.ReadFile(args[0])
425 case 2:
426 if args[0] != "-stdin" {
427 ts.Fatalf("usage: ttyin [-stdin] filename")
428 }
429 if ts.stdin != "" {
430 ts.Fatalf("conflicting use of 'stdin' and 'ttyin -stdin'")
431 }
432 ts.stdinPty = true
433 ts.ttyin = ts.ReadFile(args[1])
434 default:
435 ts.Fatalf("usage: ttyin [-stdin] filename")
436 }
437 if ts.ttyin == "" {
438 ts.Fatalf("tty input file is empty")
439 }
440 }
441
442 func (ts *TestScript) cmdTtyout(neg bool, args []string) {
443 scriptMatch(ts, neg, args, ts.ttyout, "ttyout")
444 }
445
446
447 func (ts *TestScript) cmdStop(neg bool, args []string) {
448 if neg {
449 ts.Fatalf("unsupported: ! stop")
450 }
451 if len(args) > 1 {
452 ts.Fatalf("usage: stop [msg]")
453 }
454 if len(args) == 1 {
455 ts.Logf("stop: %s\n", args[0])
456 } else {
457 ts.Logf("stop\n")
458 }
459 ts.stopped = true
460 }
461
462
463 func (ts *TestScript) cmdSymlink(neg bool, args []string) {
464 if neg {
465 ts.Fatalf("unsupported: ! symlink")
466 }
467 if len(args) != 3 || args[1] != "->" {
468 ts.Fatalf("usage: symlink file -> target")
469 }
470
471
472 ts.Check(os.Symlink(args[2], ts.MkAbs(args[0])))
473 }
474
475
476 func (ts *TestScript) cmdUNIX2DOS(neg bool, args []string) {
477 if neg {
478 ts.Fatalf("unsupported: ! unix2dos")
479 }
480 if len(args) < 1 {
481 ts.Fatalf("usage: unix2dos paths...")
482 }
483 for _, arg := range args {
484 filename := ts.MkAbs(arg)
485 data, err := ioutil.ReadFile(filename)
486 ts.Check(err)
487 dosData, err := unix2DOS(data)
488 ts.Check(err)
489 if err := ioutil.WriteFile(filename, dosData, 0o666); err != nil {
490 ts.Fatalf("%s: %v", filename, err)
491 }
492 }
493 }
494
495
496 func (ts *TestScript) cmdWait(neg bool, args []string) {
497 if len(args) > 1 {
498 ts.Fatalf("usage: wait [name]")
499 }
500 if neg {
501 ts.Fatalf("unsupported: ! wait")
502 }
503 if len(args) > 0 {
504 ts.waitBackgroundOne(args[0])
505 } else {
506 ts.waitBackground(true)
507 }
508 }
509
510 func (ts *TestScript) waitBackgroundOne(bgName string) {
511 bg := ts.findBackground(bgName)
512 if bg == nil {
513 ts.Fatalf("unknown background process %q", bgName)
514 }
515 <-bg.wait
516 ts.stdout = bg.cmd.Stdout.(*strings.Builder).String()
517 ts.stderr = bg.cmd.Stderr.(*strings.Builder).String()
518 if ts.stdout != "" {
519 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
520 }
521 if ts.stderr != "" {
522 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
523 }
524
525
526 if bg.cmd.ProcessState.Success() {
527 if bg.neg {
528 ts.Fatalf("unexpected command success")
529 }
530 } else {
531 if ts.ctxt.Err() != nil {
532 ts.Fatalf("test timed out while running command")
533 } else if !bg.neg {
534 ts.Fatalf("unexpected command failure")
535 }
536 }
537
538 for i := range ts.background {
539 if bg == &ts.background[i] {
540 ts.background = append(ts.background[:i], ts.background[i+1:]...)
541 break
542 }
543 }
544 }
545
546 func (ts *TestScript) findBackground(bgName string) *backgroundCmd {
547 if bgName == "" {
548 return nil
549 }
550 for i := range ts.background {
551 bg := &ts.background[i]
552 if bg.name == bgName {
553 return bg
554 }
555 }
556 return nil
557 }
558
559 func (ts *TestScript) waitBackground(checkStatus bool) {
560 var stdouts, stderrs []string
561 for _, bg := range ts.background {
562 <-bg.wait
563
564 args := append([]string{filepath.Base(bg.cmd.Args[0])}, bg.cmd.Args[1:]...)
565 fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.cmd.ProcessState)
566
567 cmdStdout := bg.cmd.Stdout.(*strings.Builder).String()
568 if cmdStdout != "" {
569 fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
570 stdouts = append(stdouts, cmdStdout)
571 }
572
573 cmdStderr := bg.cmd.Stderr.(*strings.Builder).String()
574 if cmdStderr != "" {
575 fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
576 stderrs = append(stderrs, cmdStderr)
577 }
578
579 if !checkStatus {
580 continue
581 }
582 if bg.cmd.ProcessState.Success() {
583 if bg.neg {
584 ts.Fatalf("unexpected command success")
585 }
586 } else {
587 if ts.ctxt.Err() != nil {
588 ts.Fatalf("test timed out while running command")
589 } else if !bg.neg {
590 ts.Fatalf("unexpected command failure")
591 }
592 }
593 }
594
595 ts.stdout = strings.Join(stdouts, "")
596 ts.stderr = strings.Join(stderrs, "")
597 ts.background = nil
598 }
599
600
601 func scriptMatch(ts *TestScript, neg bool, args []string, text, name string) {
602 n := 0
603 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
604 if neg {
605 ts.Fatalf("cannot use -count= with negated match")
606 }
607 var err error
608 n, err = strconv.Atoi(args[0][len("-count="):])
609 if err != nil {
610 ts.Fatalf("bad -count=: %v", err)
611 }
612 if n < 1 {
613 ts.Fatalf("bad -count=: must be at least 1")
614 }
615 args = args[1:]
616 }
617
618 extraUsage := ""
619 want := 1
620 if name == "grep" {
621 extraUsage = " file"
622 want = 2
623 }
624 if len(args) != want {
625 ts.Fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
626 }
627
628 pattern := args[0]
629 re, err := regexp.Compile(`(?m)` + pattern)
630 ts.Check(err)
631
632 isGrep := name == "grep"
633 if isGrep {
634 name = args[1]
635 data, err := ioutil.ReadFile(ts.MkAbs(args[1]))
636 ts.Check(err)
637 text = string(data)
638 }
639
640 if neg {
641 if re.MatchString(text) {
642 if isGrep {
643 ts.Logf("[%s]\n%s\n", name, text)
644 }
645 ts.Fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
646 }
647 } else {
648 if !re.MatchString(text) {
649 if isGrep {
650 ts.Logf("[%s]\n%s\n", name, text)
651 }
652 ts.Fatalf("no match for %#q found in %s", pattern, name)
653 }
654 if n > 0 {
655 count := len(re.FindAllString(text, -1))
656 if count != n {
657 if isGrep {
658 ts.Logf("[%s]\n%s\n", name, text)
659 }
660 ts.Fatalf("have %d matches for %#q, want %d", count, pattern, n)
661 }
662 }
663 }
664 }
665
666
667 func unix2DOS(data []byte) ([]byte, error) {
668 sb := &strings.Builder{}
669 s := bufio.NewScanner(bytes.NewReader(data))
670 for s.Scan() {
671 if _, err := sb.Write(s.Bytes()); err != nil {
672 return nil, err
673 }
674 if _, err := sb.WriteString("\r\n"); err != nil {
675 return nil, err
676 }
677 }
678 if err := s.Err(); err != nil {
679 return nil, err
680 }
681 return []byte(sb.String()), nil
682 }
683
View as plain text