1
2
3
4
5
6
7
8 package testscript
9
10 import (
11 "bytes"
12 "context"
13 "errors"
14 "flag"
15 "fmt"
16 "go/build"
17 "io"
18 "io/fs"
19 "os"
20 "os/exec"
21 "path/filepath"
22 "regexp"
23 "runtime"
24 "sort"
25 "strings"
26 "sync/atomic"
27 "syscall"
28 "testing"
29 "time"
30
31 "github.com/rogpeppe/go-internal/imports"
32 "github.com/rogpeppe/go-internal/internal/misspell"
33 "github.com/rogpeppe/go-internal/internal/os/execpath"
34 "github.com/rogpeppe/go-internal/par"
35 "github.com/rogpeppe/go-internal/testenv"
36 "github.com/rogpeppe/go-internal/testscript/internal/pty"
37 "github.com/rogpeppe/go-internal/txtar"
38 )
39
40 var goVersionRegex = regexp.MustCompile(`^go([1-9][0-9]*)\.([1-9][0-9]*)$`)
41
42 var execCache par.Cache
43
44
45
46
47 var testWork = flag.Bool("testwork", false, "")
48
49
50
51
52 var timeSince = time.Since
53
54
55
56
57 var showVerboseEnv = true
58
59
60 type Env struct {
61
62
63 WorkDir string
64
65
66 Vars []string
67
68 Cd string
69
70
71
72 Values map[interface{}]interface{}
73
74 ts *TestScript
75 }
76
77
78
79 func (ts *TestScript) Value(key interface{}) interface{} {
80 return ts.values[key]
81 }
82
83
84
85
86
87 func (e *Env) Defer(f func()) {
88 e.ts.Defer(f)
89 }
90
91
92
93 func (e *Env) Getenv(key string) string {
94 key = envvarname(key)
95 for i := len(e.Vars) - 1; i >= 0; i-- {
96 if pair := strings.SplitN(e.Vars[i], "=", 2); len(pair) == 2 && envvarname(pair[0]) == key {
97 return pair[1]
98 }
99 }
100 return ""
101 }
102
103 func envvarname(k string) string {
104 if runtime.GOOS == "windows" {
105 return strings.ToLower(k)
106 }
107 return k
108 }
109
110
111
112 func (e *Env) Setenv(key, value string) {
113 if key == "" || strings.IndexByte(key, '=') != -1 {
114 panic(fmt.Errorf("invalid environment variable key %q", key))
115 }
116 e.Vars = append(e.Vars, key+"="+value)
117 }
118
119
120
121
122
123
124
125
126
127 func (e *Env) T() T {
128 return e.ts.t
129 }
130
131
132 type Params struct {
133
134
135
136
137 Dir string
138
139
140
141
142
143
144 Setup func(*Env) error
145
146
147
148
149 Condition func(cond string) (bool, error)
150
151
152
153 Cmds map[string]func(ts *TestScript, neg bool, args []string)
154
155
156
157 TestWork bool
158
159
160
161
162
163 WorkdirRoot string
164
165
166 IgnoreMissedCoverage bool
167
168
169
170
171
172
173
174
175
176 UpdateScripts bool
177
178
179
180
181
182 RequireExplicitExec bool
183
184
185
186 RequireUniqueNames bool
187
188
189
190
191 ContinueOnError bool
192
193
194
195
196 Deadline time.Time
197 }
198
199
200
201 func Run(t *testing.T, p Params) {
202 if deadline, ok := t.Deadline(); ok && p.Deadline.IsZero() {
203 p.Deadline = deadline
204 }
205 RunT(tshim{t}, p)
206 }
207
208
209
210 type T interface {
211 Skip(...interface{})
212 Fatal(...interface{})
213 Parallel()
214 Log(...interface{})
215 FailNow()
216 Run(string, func(T))
217
218
219 Verbose() bool
220 }
221
222
223
224 type TFailed interface {
225 Failed() bool
226 }
227
228 type tshim struct {
229 *testing.T
230 }
231
232 func (t tshim) Run(name string, f func(T)) {
233 t.T.Run(name, func(t *testing.T) {
234 f(tshim{t})
235 })
236 }
237
238 func (t tshim) Verbose() bool {
239 return testing.Verbose()
240 }
241
242
243
244 func RunT(t T, p Params) {
245 entries, err := os.ReadDir(p.Dir)
246 if os.IsNotExist(err) {
247
248 } else if err != nil {
249 t.Fatal(err)
250 }
251 var files []string
252 for _, entry := range entries {
253 name := entry.Name()
254 if strings.HasSuffix(name, ".txtar") || strings.HasSuffix(name, ".txt") {
255 files = append(files, filepath.Join(p.Dir, name))
256 }
257 }
258
259 if len(files) == 0 {
260 t.Fatal(fmt.Sprintf("no txtar nor txt scripts found in dir %s", p.Dir))
261 }
262 testTempDir := p.WorkdirRoot
263 if testTempDir == "" {
264 testTempDir, err = os.MkdirTemp(os.Getenv("GOTMPDIR"), "go-test-script")
265 if err != nil {
266 t.Fatal(err)
267 }
268 } else {
269 p.TestWork = true
270 }
271
272
273
274
275 testTempDir, err = filepath.EvalSymlinks(testTempDir)
276 if err != nil {
277 t.Fatal(err)
278 }
279
280 var (
281 ctx = context.Background()
282 gracePeriod = 100 * time.Millisecond
283 cancel context.CancelFunc
284 )
285 if !p.Deadline.IsZero() {
286 timeout := time.Until(p.Deadline)
287
288
289
290 if gp := timeout / 20; gp > gracePeriod {
291 gracePeriod = gp
292 }
293
294
295
296
297
298
299
300
301 timeout -= 2 * gracePeriod
302
303 ctx, cancel = context.WithTimeout(ctx, timeout)
304
305
306
307 _ = cancel
308 }
309
310 refCount := int32(len(files))
311 for _, file := range files {
312 file := file
313 name := strings.TrimSuffix(filepath.Base(file), ".txt")
314 name = strings.TrimSuffix(name, ".txtar")
315 t.Run(name, func(t T) {
316 t.Parallel()
317 ts := &TestScript{
318 t: t,
319 testTempDir: testTempDir,
320 name: name,
321 file: file,
322 params: p,
323 ctxt: ctx,
324 gracePeriod: gracePeriod,
325 deferred: func() {},
326 scriptFiles: make(map[string]string),
327 scriptUpdates: make(map[string]string),
328 }
329 defer func() {
330 if p.TestWork || *testWork {
331 return
332 }
333 removeAll(ts.workdir)
334 if atomic.AddInt32(&refCount, -1) == 0 {
335
336
337 os.Remove(testTempDir)
338 if cancel != nil {
339 cancel()
340 }
341 }
342 }()
343 ts.run()
344 })
345 }
346 }
347
348
349 type TestScript struct {
350 params Params
351 t T
352 testTempDir string
353 workdir string
354 log bytes.Buffer
355 mark int
356 cd string
357 name string
358 file string
359 lineno int
360 line string
361 env []string
362 envMap map[string]string
363 values map[interface{}]interface{}
364 stdin string
365 stdout string
366 stderr string
367 ttyin string
368 stdinPty bool
369 ttyout string
370 stopped bool
371 start time.Time
372 background []backgroundCmd
373 deferred func()
374 archive *txtar.Archive
375 scriptFiles map[string]string
376 scriptUpdates map[string]string
377
378
379
380 runningBuiltin bool
381
382
383
384
385
386 builtinStdout *strings.Builder
387 builtinStderr *strings.Builder
388
389 ctxt context.Context
390 gracePeriod time.Duration
391 }
392
393 type backgroundCmd struct {
394 name string
395 cmd *exec.Cmd
396 wait <-chan struct{}
397 neg bool
398 }
399
400 func writeFile(name string, data []byte, perm fs.FileMode, excl bool) error {
401 oflags := os.O_WRONLY | os.O_CREATE | os.O_TRUNC
402 if excl {
403 oflags |= os.O_EXCL
404 }
405 f, err := os.OpenFile(name, oflags, perm)
406 if err != nil {
407 return err
408 }
409 defer f.Close()
410 if _, err := f.Write(data); err != nil {
411 return fmt.Errorf("cannot write file contents: %v", err)
412 }
413 return nil
414 }
415
416
417 func (ts *TestScript) Name() string { return ts.name }
418
419
420
421 func (ts *TestScript) setup() string {
422 defer catchFailNow(func() {
423
424
425 ts.t.FailNow()
426 })
427 ts.workdir = filepath.Join(ts.testTempDir, "script-"+ts.name)
428
429
430
431
432
433
434
435
436 tmpDir := filepath.Join(ts.workdir, ".tmp")
437
438 ts.Check(os.MkdirAll(tmpDir, 0o777))
439 env := &Env{
440 Vars: []string{
441 "WORK=" + ts.workdir,
442 "PATH=" + os.Getenv("PATH"),
443 "GOTRACEBACK=system",
444 homeEnvName() + "=/no-home",
445 tempEnvName() + "=" + tmpDir,
446 "devnull=" + os.DevNull,
447 "/=" + string(os.PathSeparator),
448 ":=" + string(os.PathListSeparator),
449 "$=$",
450 },
451 WorkDir: ts.workdir,
452 Values: make(map[interface{}]interface{}),
453 Cd: ts.workdir,
454 ts: ts,
455 }
456
457
458
459
460 for _, name := range []string{
461
462 "GOCOVERDIR",
463
464
465 "GORACE",
466 } {
467 if val := os.Getenv(name); val != "" {
468 env.Vars = append(env.Vars, name+"="+val)
469 }
470 }
471
472 if runtime.GOOS == "windows" {
473 env.Vars = append(env.Vars,
474 "SYSTEMROOT="+os.Getenv("SYSTEMROOT"),
475 "exe=.exe",
476 )
477 } else {
478 env.Vars = append(env.Vars,
479 "exe=",
480 )
481 }
482 ts.cd = env.Cd
483
484 a, err := txtar.ParseFile(ts.file)
485 ts.Check(err)
486 ts.archive = a
487 for _, f := range a.Files {
488 name := ts.MkAbs(ts.expand(f.Name))
489 ts.scriptFiles[name] = f.Name
490 ts.Check(os.MkdirAll(filepath.Dir(name), 0o777))
491 switch err := writeFile(name, f.Data, 0o666, ts.params.RequireUniqueNames); {
492 case ts.params.RequireUniqueNames && errors.Is(err, fs.ErrExist):
493 ts.Check(fmt.Errorf("%s would overwrite %s (because RequireUniqueNames is enabled)", f.Name, name))
494 default:
495 ts.Check(err)
496 }
497 }
498
499 if ts.params.Setup != nil {
500 ts.Check(ts.params.Setup(env))
501 }
502 ts.cd = env.Cd
503 ts.env = env.Vars
504 ts.values = env.Values
505
506 ts.envMap = make(map[string]string)
507 for _, kv := range ts.env {
508 if i := strings.Index(kv, "="); i >= 0 {
509 ts.envMap[envvarname(kv[:i])] = kv[i+1:]
510 }
511 }
512 return string(a.Comment)
513 }
514
515
516 func (ts *TestScript) run() {
517
518
519 verbose := ts.t.Verbose()
520 rewind := func() {
521 if !verbose {
522 ts.log.Truncate(ts.mark)
523 }
524 }
525
526
527 markTime := func() {
528 if ts.mark > 0 && !ts.start.IsZero() {
529 afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
530 ts.log.Truncate(ts.mark - 1)
531 fmt.Fprintf(&ts.log, " (%.3fs)\n", timeSince(ts.start).Seconds())
532 ts.log.Write(afterMark)
533 }
534 ts.start = time.Time{}
535 }
536
537 failed := false
538 defer func() {
539
540
541
542 for _, bg := range ts.background {
543 interruptProcess(bg.cmd.Process)
544 }
545 if ts.t.Verbose() || failed {
546
547
548 ts.waitBackground(false)
549 } else {
550 for _, bg := range ts.background {
551 <-bg.wait
552 }
553 ts.background = nil
554 }
555
556 markTime()
557
558 ts.t.Log(ts.abbrev(ts.log.String()))
559 }()
560 defer func() {
561 ts.deferred()
562 }()
563 script := ts.setup()
564
565
566 if *testWork || (showVerboseEnv && ts.t.Verbose()) {
567
568 ts.cmdEnv(false, nil)
569 fmt.Fprintf(&ts.log, "\n")
570 ts.mark = ts.log.Len()
571 }
572 defer ts.applyScriptUpdates()
573
574
575
576 for script != "" {
577
578 ts.lineno++
579 var line string
580 if i := strings.Index(script, "\n"); i >= 0 {
581 line, script = script[:i], script[i+1:]
582 } else {
583 line, script = script, ""
584 }
585
586
587 if strings.HasPrefix(line, "#") {
588
589
590
591
592
593
594
595 if ts.log.Len() > ts.mark {
596 rewind()
597 markTime()
598 }
599
600 fmt.Fprintf(&ts.log, "%s\n", line)
601 ts.mark = ts.log.Len()
602 ts.start = time.Now()
603 continue
604 }
605
606 ok := ts.runLine(line)
607 if !ok {
608 failed = true
609 if ts.params.ContinueOnError {
610 verbose = true
611 } else {
612 ts.t.FailNow()
613 }
614 }
615
616
617 if ts.stopped {
618
619
620 break
621 }
622 }
623
624 for _, bg := range ts.background {
625 interruptProcess(bg.cmd.Process)
626 }
627 ts.cmdWait(false, nil)
628
629
630
631 if failed {
632 ts.t.FailNow()
633 }
634
635
636 rewind()
637 markTime()
638 if !ts.stopped {
639 fmt.Fprintf(&ts.log, "PASS\n")
640 }
641 }
642
643 func (ts *TestScript) runLine(line string) (runOK bool) {
644 defer catchFailNow(func() {
645 runOK = false
646 })
647
648
649 args := ts.parse(line)
650 if len(args) == 0 {
651 return true
652 }
653
654
655 fmt.Fprintf(&ts.log, "> %s\n", line)
656
657
658 for strings.HasPrefix(args[0], "[") && strings.HasSuffix(args[0], "]") {
659 cond := args[0]
660 cond = cond[1 : len(cond)-1]
661 cond = strings.TrimSpace(cond)
662 args = args[1:]
663 if len(args) == 0 {
664 ts.Fatalf("missing command after condition")
665 }
666 want := true
667 if strings.HasPrefix(cond, "!") {
668 want = false
669 cond = strings.TrimSpace(cond[1:])
670 }
671 ok, err := ts.condition(cond)
672 if err != nil {
673 ts.Fatalf("bad condition %q: %v", cond, err)
674 }
675 if ok != want {
676
677 return true
678 }
679 }
680
681
682
683 neg := false
684 if args[0] == "!" {
685 neg = true
686 args = args[1:]
687 if len(args) == 0 {
688 ts.Fatalf("! on line by itself")
689 }
690 }
691
692
693 cmd := scriptCmds[args[0]]
694 if cmd == nil {
695 cmd = ts.params.Cmds[args[0]]
696 }
697 if cmd == nil {
698
699
700 switch c := ts.cmdSuggestions(args[0]); len(c) {
701 case 1:
702 ts.Fatalf("unknown command %q (did you mean %q?)", args[0], c[0])
703 case 2, 3, 4:
704 ts.Fatalf("unknown command %q (did you mean one of %q?)", args[0], c)
705 default:
706 ts.Fatalf("unknown command %q", args[0])
707 }
708 }
709 ts.callBuiltinCmd(args[0], func() {
710 cmd(ts, neg, args[1:])
711 })
712 return true
713 }
714
715 func (ts *TestScript) callBuiltinCmd(cmd string, runCmd func()) {
716 ts.runningBuiltin = true
717 defer func() {
718 r := recover()
719 ts.runningBuiltin = false
720 ts.clearBuiltinStd()
721 switch r {
722 case nil:
723
724 default:
725
726 panic(r)
727 }
728 }()
729 runCmd()
730 }
731
732 func (ts *TestScript) cmdSuggestions(name string) []string {
733
734 if strings.HasPrefix(name, "!") {
735 if _, ok := scriptCmds[name[1:]]; ok {
736 return []string{"! " + name[1:]}
737 }
738 if _, ok := ts.params.Cmds[name[1:]]; ok {
739 return []string{"! " + name[1:]}
740 }
741 }
742 var candidates []string
743 for c := range scriptCmds {
744 if misspell.AlmostEqual(name, c) {
745 candidates = append(candidates, c)
746 }
747 }
748 for c := range ts.params.Cmds {
749 if misspell.AlmostEqual(name, c) {
750 candidates = append(candidates, c)
751 }
752 }
753 if len(candidates) == 0 {
754 return nil
755 }
756
757
758 sort.Strings(candidates)
759 out := candidates[:1]
760 for _, c := range candidates[1:] {
761 if out[len(out)-1] == c {
762 out = append(out, c)
763 }
764 }
765 return out
766 }
767
768 func (ts *TestScript) applyScriptUpdates() {
769 if len(ts.scriptUpdates) == 0 {
770 return
771 }
772 for name, content := range ts.scriptUpdates {
773 found := false
774 for i := range ts.archive.Files {
775 f := &ts.archive.Files[i]
776 if f.Name != name {
777 continue
778 }
779 data := []byte(content)
780 if txtar.NeedsQuote(data) {
781 data1, err := txtar.Quote(data)
782 if err != nil {
783 ts.Fatalf("cannot update script file %q: %v", f.Name, err)
784 continue
785 }
786 data = data1
787 }
788 f.Data = data
789 found = true
790 }
791
792 if !found {
793 panic("script update file not found")
794 }
795 }
796 if err := os.WriteFile(ts.file, txtar.Format(ts.archive), 0o666); err != nil {
797 ts.t.Fatal("cannot update script: ", err)
798 }
799 ts.Logf("%s updated", ts.file)
800 }
801
802 var failNow = errors.New("fail now!")
803
804
805
806 func catchFailNow(f func()) {
807 e := recover()
808 if e == nil {
809 return
810 }
811 if e != failNow {
812 panic(e)
813 }
814 f()
815 }
816
817
818 func (ts *TestScript) condition(cond string) (bool, error) {
819 switch {
820 case cond == "short":
821 return testing.Short(), nil
822 case cond == "net":
823 return testenv.HasExternalNetwork(), nil
824 case cond == "link":
825 return testenv.HasLink(), nil
826 case cond == "symlink":
827 return testenv.HasSymlink(), nil
828 case imports.KnownOS[cond]:
829 return cond == runtime.GOOS, nil
830 case cond == "unix":
831 return imports.UnixOS[runtime.GOOS], nil
832 case imports.KnownArch[cond]:
833 return cond == runtime.GOARCH, nil
834 case strings.HasPrefix(cond, "exec:"):
835 prog := cond[len("exec:"):]
836 ok := execCache.Do(prog, func() interface{} {
837 _, err := execpath.Look(prog, ts.Getenv)
838 return err == nil
839 }).(bool)
840 return ok, nil
841 case cond == "gc" || cond == "gccgo":
842
843
844
845 return cond == runtime.Compiler, nil
846 case goVersionRegex.MatchString(cond):
847 for _, v := range build.Default.ReleaseTags {
848 if cond == v {
849 return true, nil
850 }
851 }
852 return false, nil
853 case ts.params.Condition != nil:
854 return ts.params.Condition(cond)
855 default:
856 ts.Fatalf("unknown condition %q", cond)
857 panic("unreachable")
858 }
859 }
860
861
862
863
864 func (ts *TestScript) abbrev(s string) string {
865 s = strings.Replace(s, ts.workdir, "$WORK", -1)
866 if *testWork || ts.params.TestWork {
867
868
869 s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n")
870 }
871 return s
872 }
873
874
875
876
877
878 func (ts *TestScript) Defer(f func()) {
879 old := ts.deferred
880 ts.deferred = func() {
881 defer old()
882 f()
883 }
884 }
885
886
887 func (ts *TestScript) Check(err error) {
888 if err != nil {
889 ts.Fatalf("%v", err)
890 }
891 }
892
893
894
895
896
897 func (ts *TestScript) Stdout() io.Writer {
898 if !ts.runningBuiltin {
899 panic("can only call TestScript.Stdout when running a builtin command")
900 }
901 ts.setBuiltinStd()
902 return ts.builtinStdout
903 }
904
905
906
907
908
909 func (ts *TestScript) Stderr() io.Writer {
910 if !ts.runningBuiltin {
911 panic("can only call TestScript.Stderr when running a builtin command")
912 }
913 ts.setBuiltinStd()
914 return ts.builtinStderr
915 }
916
917
918 func (ts *TestScript) setBuiltinStd() {
919
920
921
922
923 if ts.builtinStdout != nil && ts.builtinStderr != nil {
924 return
925 }
926 ts.builtinStdout = new(strings.Builder)
927 ts.builtinStderr = new(strings.Builder)
928 }
929
930
931
932 func (ts *TestScript) clearBuiltinStd() {
933
934
935
936
937 if ts.builtinStdout == nil && ts.builtinStderr == nil {
938 return
939 }
940 ts.stdout = ts.builtinStdout.String()
941 ts.builtinStdout = nil
942 ts.stderr = ts.builtinStderr.String()
943 ts.builtinStderr = nil
944 ts.logStd()
945 }
946
947
948 func (ts *TestScript) Logf(format string, args ...interface{}) {
949 format = strings.TrimSuffix(format, "\n")
950 fmt.Fprintf(&ts.log, format, args...)
951 ts.log.WriteByte('\n')
952 }
953
954
955
956 func (ts *TestScript) exec(command string, args ...string) (stdout, stderr string, err error) {
957 cmd, err := ts.buildExecCmd(command, args...)
958 if err != nil {
959 return "", "", err
960 }
961 cmd.Dir = ts.cd
962 cmd.Env = append(ts.env, "PWD="+ts.cd)
963 cmd.Stdin = strings.NewReader(ts.stdin)
964 var stdoutBuf, stderrBuf strings.Builder
965 cmd.Stdout = &stdoutBuf
966 cmd.Stderr = &stderrBuf
967 if ts.ttyin != "" {
968 ctrl, tty, err := pty.Open()
969 if err != nil {
970 return "", "", err
971 }
972 doneR, doneW := make(chan struct{}), make(chan struct{})
973 var ptyBuf strings.Builder
974 go func() {
975 io.Copy(ctrl, strings.NewReader(ts.ttyin))
976 ctrl.Write([]byte{4 })
977 close(doneW)
978 }()
979 go func() {
980 io.Copy(&ptyBuf, ctrl)
981 close(doneR)
982 }()
983 defer func() {
984 tty.Close()
985 ctrl.Close()
986 <-doneR
987 <-doneW
988 ts.ttyin = ""
989 ts.ttyout = ptyBuf.String()
990 }()
991 pty.SetCtty(cmd, tty)
992 if ts.stdinPty {
993 cmd.Stdin = tty
994 }
995 }
996 if err = cmd.Start(); err == nil {
997 err = waitOrStop(ts.ctxt, cmd, ts.gracePeriod)
998 }
999 ts.stdin = ""
1000 ts.stdinPty = false
1001 return stdoutBuf.String(), stderrBuf.String(), err
1002 }
1003
1004
1005
1006 func (ts *TestScript) execBackground(command string, args ...string) (*exec.Cmd, error) {
1007 if ts.ttyin != "" {
1008 return nil, errors.New("ttyin is not supported by background commands")
1009 }
1010 cmd, err := ts.buildExecCmd(command, args...)
1011 if err != nil {
1012 return nil, err
1013 }
1014 cmd.Dir = ts.cd
1015 cmd.Env = append(ts.env, "PWD="+ts.cd)
1016 var stdoutBuf, stderrBuf strings.Builder
1017 cmd.Stdin = strings.NewReader(ts.stdin)
1018 cmd.Stdout = &stdoutBuf
1019 cmd.Stderr = &stderrBuf
1020 ts.stdin = ""
1021 return cmd, cmd.Start()
1022 }
1023
1024 func (ts *TestScript) buildExecCmd(command string, args ...string) (*exec.Cmd, error) {
1025 if filepath.Base(command) == command {
1026 if lp, err := execpath.Look(command, ts.Getenv); err != nil {
1027 return nil, err
1028 } else {
1029 command = lp
1030 }
1031 }
1032 return exec.Command(command, args...), nil
1033 }
1034
1035
1036
1037
1038 func (ts *TestScript) BackgroundCmds() []*exec.Cmd {
1039 cmds := make([]*exec.Cmd, len(ts.background))
1040 for i, b := range ts.background {
1041 cmds[i] = b.cmd
1042 }
1043 return cmds
1044 }
1045
1046
1047
1048
1049
1050
1051 func waitOrStop(ctx context.Context, cmd *exec.Cmd, killDelay time.Duration) error {
1052 if cmd.Process == nil {
1053 panic("waitOrStop called with a nil cmd.Process — missing Start call?")
1054 }
1055
1056 errc := make(chan error)
1057 go func() {
1058 select {
1059 case errc <- nil:
1060 return
1061 case <-ctx.Done():
1062 }
1063
1064 var interrupt os.Signal = syscall.SIGQUIT
1065 if runtime.GOOS == "windows" {
1066
1067
1068
1069 interrupt = os.Kill
1070 }
1071
1072 err := cmd.Process.Signal(interrupt)
1073 if err == nil {
1074 err = ctx.Err()
1075 } else if err == os.ErrProcessDone {
1076 errc <- nil
1077 return
1078 }
1079
1080 if killDelay > 0 {
1081 timer := time.NewTimer(killDelay)
1082 select {
1083
1084 case errc <- ctx.Err():
1085 timer.Stop()
1086 return
1087
1088 case <-timer.C:
1089 }
1090
1091
1092
1093
1094
1095
1096
1097 _ = cmd.Process.Kill()
1098 }
1099
1100 errc <- err
1101 }()
1102
1103 waitErr := cmd.Wait()
1104 if interruptErr := <-errc; interruptErr != nil {
1105 return interruptErr
1106 }
1107 return waitErr
1108 }
1109
1110
1111 func interruptProcess(p *os.Process) {
1112 if err := p.Signal(os.Interrupt); err != nil {
1113
1114
1115
1116 p.Kill()
1117 }
1118 }
1119
1120
1121
1122 func (ts *TestScript) Exec(command string, args ...string) error {
1123 var err error
1124 ts.stdout, ts.stderr, err = ts.exec(command, args...)
1125 ts.logStd()
1126 return err
1127 }
1128
1129
1130 func (ts *TestScript) logStd() {
1131 if ts.stdout != "" {
1132 ts.Logf("[stdout]\n%s", ts.stdout)
1133 }
1134 if ts.stderr != "" {
1135 ts.Logf("[stderr]\n%s", ts.stderr)
1136 }
1137 }
1138
1139
1140 func (ts *TestScript) expand(s string) string {
1141 return os.Expand(s, func(key string) string {
1142 if key1 := strings.TrimSuffix(key, "@R"); len(key1) != len(key) {
1143 return regexp.QuoteMeta(ts.Getenv(key1))
1144 }
1145 return ts.Getenv(key)
1146 })
1147 }
1148
1149
1150 func (ts *TestScript) Fatalf(format string, args ...interface{}) {
1151
1152
1153
1154
1155 ts.clearBuiltinStd()
1156
1157 fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
1158
1159
1160
1161 panic(failNow)
1162 }
1163
1164
1165
1166 func (ts *TestScript) MkAbs(file string) string {
1167 if filepath.IsAbs(file) {
1168 return file
1169 }
1170 return filepath.Join(ts.cd, file)
1171 }
1172
1173
1174
1175
1176
1177
1178
1179
1180 func (ts *TestScript) ReadFile(file string) string {
1181 switch file {
1182 case "stdout":
1183 return ts.stdout
1184 case "stderr":
1185 return ts.stderr
1186 case "ttyout":
1187 return ts.ttyout
1188 default:
1189 file = ts.MkAbs(file)
1190 data, err := os.ReadFile(file)
1191 ts.Check(err)
1192 return string(data)
1193 }
1194 }
1195
1196
1197 func (ts *TestScript) Setenv(key, value string) {
1198 ts.env = append(ts.env, key+"="+value)
1199 ts.envMap[envvarname(key)] = value
1200 }
1201
1202
1203 func (ts *TestScript) Getenv(key string) string {
1204 return ts.envMap[envvarname(key)]
1205 }
1206
1207
1208
1209
1210
1211 func (ts *TestScript) parse(line string) []string {
1212 ts.line = line
1213
1214 var (
1215 args []string
1216 arg string
1217 start = -1
1218 quoted = false
1219 )
1220 for i := 0; ; i++ {
1221 if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
1222
1223 if start >= 0 {
1224 arg += ts.expand(line[start:i])
1225 args = append(args, arg)
1226 start = -1
1227 arg = ""
1228 }
1229 if i >= len(line) || line[i] == '#' {
1230 break
1231 }
1232 continue
1233 }
1234 if i >= len(line) {
1235 ts.Fatalf("unterminated quoted argument")
1236 }
1237 if line[i] == '\'' {
1238 if !quoted {
1239
1240 if start >= 0 {
1241 arg += ts.expand(line[start:i])
1242 }
1243 start = i + 1
1244 quoted = true
1245 continue
1246 }
1247
1248 if i+1 < len(line) && line[i+1] == '\'' {
1249 arg += line[start:i]
1250 start = i + 1
1251 i++
1252 continue
1253 }
1254
1255 arg += line[start:i]
1256 start = i + 1
1257 quoted = false
1258 continue
1259 }
1260
1261 if start < 0 {
1262 start = i
1263 }
1264 }
1265 return args
1266 }
1267
1268 func removeAll(dir string) error {
1269
1270
1271 filepath.WalkDir(dir, func(path string, entry fs.DirEntry, err error) error {
1272 if err != nil {
1273 return nil
1274 }
1275 if entry.IsDir() {
1276 os.Chmod(path, 0o777)
1277 }
1278 return nil
1279 })
1280 return os.RemoveAll(dir)
1281 }
1282
1283 func homeEnvName() string {
1284 switch runtime.GOOS {
1285 case "windows":
1286 return "USERPROFILE"
1287 case "plan9":
1288 return "home"
1289 default:
1290 return "HOME"
1291 }
1292 }
1293
1294 func tempEnvName() string {
1295 switch runtime.GOOS {
1296 case "windows":
1297 return "TMP"
1298 case "plan9":
1299 return "TMPDIR"
1300 default:
1301 return "TMPDIR"
1302 }
1303 }
1304
View as plain text