Source file
src/cmd/go/script_test.go
1
2
3
4
5
6
7
8 package main_test
9
10 import (
11 "bytes"
12 "context"
13 "errors"
14 "flag"
15 "fmt"
16 "go/build"
17 "internal/testenv"
18 "io/fs"
19 "os"
20 "os/exec"
21 "path/filepath"
22 "regexp"
23 "runtime"
24 "strconv"
25 "strings"
26 "sync"
27 "testing"
28 "time"
29
30 "cmd/go/internal/cfg"
31 "cmd/go/internal/imports"
32 "cmd/go/internal/par"
33 "cmd/go/internal/robustio"
34 "cmd/go/internal/work"
35 "cmd/internal/sys"
36
37 "golang.org/x/tools/txtar"
38 )
39
40 var testSum = flag.String("testsum", "", `may be tidy, listm, or listall. If set, TestScript generates a go.sum file at the beginning of each test and updates test files if they pass.`)
41
42
43 func TestScript(t *testing.T) {
44 testenv.MustHaveGoBuild(t)
45 testenv.SkipIfShortAndSlow(t)
46
47 var (
48 ctx = context.Background()
49 gracePeriod = 100 * time.Millisecond
50 )
51 if deadline, ok := t.Deadline(); ok {
52 timeout := time.Until(deadline)
53
54
55
56 if gp := timeout / 20; gp > gracePeriod {
57 gracePeriod = gp
58 }
59
60
61
62
63
64
65
66
67 timeout -= 2 * gracePeriod
68
69 var cancel context.CancelFunc
70 ctx, cancel = context.WithTimeout(ctx, timeout)
71 t.Cleanup(cancel)
72 }
73
74 files, err := filepath.Glob("testdata/script/*.txt")
75 if err != nil {
76 t.Fatal(err)
77 }
78 for _, file := range files {
79 file := file
80 name := strings.TrimSuffix(filepath.Base(file), ".txt")
81 t.Run(name, func(t *testing.T) {
82 t.Parallel()
83 ctx, cancel := context.WithCancel(ctx)
84 ts := &testScript{
85 t: t,
86 ctx: ctx,
87 cancel: cancel,
88 gracePeriod: gracePeriod,
89 name: name,
90 file: file,
91 }
92 ts.setup()
93 if !*testWork {
94 defer removeAll(ts.workdir)
95 }
96 ts.run()
97 cancel()
98 })
99 }
100 }
101
102
103 type testScript struct {
104 t *testing.T
105 ctx context.Context
106 cancel context.CancelFunc
107 gracePeriod time.Duration
108 workdir string
109 log bytes.Buffer
110 mark int
111 cd string
112 name string
113 file string
114 lineno int
115 line string
116 env []string
117 envMap map[string]string
118 stdout string
119 stderr string
120 stopped bool
121 start time.Time
122 background []*backgroundCmd
123 }
124
125 type backgroundCmd struct {
126 want simpleStatus
127 args []string
128 done <-chan struct{}
129 err error
130 stdout, stderr strings.Builder
131 }
132
133 type simpleStatus string
134
135 const (
136 success simpleStatus = ""
137 failure simpleStatus = "!"
138 successOrFailure simpleStatus = "?"
139 )
140
141 var extraEnvKeys = []string{
142 "SYSTEMROOT",
143 "WINDIR",
144 "LD_LIBRARY_PATH",
145 "LIBRARY_PATH",
146 "C_INCLUDE_PATH",
147 "CC",
148 "GO_TESTING_GOTOOLS",
149 "GCCGO",
150 "GCCGOTOOLDIR",
151 }
152
153
154 func (ts *testScript) setup() {
155 if err := ts.ctx.Err(); err != nil {
156 ts.t.Fatalf("test interrupted during setup: %v", err)
157 }
158
159 StartProxy()
160 ts.workdir = filepath.Join(testTmpDir, "script-"+ts.name)
161 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "tmp"), 0777))
162 ts.check(os.MkdirAll(filepath.Join(ts.workdir, "gopath/src"), 0777))
163 ts.cd = filepath.Join(ts.workdir, "gopath/src")
164 ts.env = []string{
165 "WORK=" + ts.workdir,
166 "PATH=" + testBin + string(filepath.ListSeparator) + os.Getenv("PATH"),
167 homeEnvName() + "=/no-home",
168 "CCACHE_DISABLE=1",
169 "GOARCH=" + runtime.GOARCH,
170 "GOCACHE=" + testGOCACHE,
171 "GODEBUG=" + os.Getenv("GODEBUG"),
172 "GOEXE=" + cfg.ExeSuffix,
173 "GOOS=" + runtime.GOOS,
174 "GOPATH=" + filepath.Join(ts.workdir, "gopath"),
175 "GOPROXY=" + proxyURL,
176 "GOPRIVATE=",
177 "GOROOT=" + testGOROOT,
178 "GOROOT_FINAL=" + os.Getenv("GOROOT_FINAL"),
179 "GOTRACEBACK=system",
180 "TESTGO_GOROOT=" + testGOROOT,
181 "GOSUMDB=" + testSumDBVerifierKey,
182 "GONOPROXY=",
183 "GONOSUMDB=",
184 "GOVCS=*:all",
185 "PWD=" + ts.cd,
186 tempEnvName() + "=" + filepath.Join(ts.workdir, "tmp"),
187 "devnull=" + os.DevNull,
188 "goversion=" + goVersion(ts),
189 ":=" + string(os.PathListSeparator),
190 "/=" + string(os.PathSeparator),
191 }
192 if !testenv.HasExternalNetwork() {
193 ts.env = append(ts.env, "TESTGONETWORK=panic", "TESTGOVCS=panic")
194 }
195
196 if runtime.GOOS == "plan9" {
197 ts.env = append(ts.env, "path="+testBin+string(filepath.ListSeparator)+os.Getenv("path"))
198 }
199
200 for _, key := range extraEnvKeys {
201 if val := os.Getenv(key); val != "" {
202 ts.env = append(ts.env, key+"="+val)
203 }
204 }
205
206 ts.envMap = make(map[string]string)
207 for _, kv := range ts.env {
208 if i := strings.Index(kv, "="); i >= 0 {
209 ts.envMap[kv[:i]] = kv[i+1:]
210 }
211 }
212 }
213
214
215 func goVersion(ts *testScript) string {
216 tags := build.Default.ReleaseTags
217 version := tags[len(tags)-1]
218 if !regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`).MatchString(version) {
219 ts.fatalf("invalid go version %q", version)
220 }
221 return version[2:]
222 }
223
224 var execCache par.Cache
225
226
227 func (ts *testScript) run() {
228
229
230 rewind := func() {
231 if !testing.Verbose() {
232 ts.log.Truncate(ts.mark)
233 }
234 }
235
236
237 markTime := func() {
238 if ts.mark > 0 && !ts.start.IsZero() {
239 afterMark := append([]byte{}, ts.log.Bytes()[ts.mark:]...)
240 ts.log.Truncate(ts.mark - 1)
241 fmt.Fprintf(&ts.log, " (%.3fs)\n", time.Since(ts.start).Seconds())
242 ts.log.Write(afterMark)
243 }
244 ts.start = time.Time{}
245 }
246
247 defer func() {
248
249
250
251 ts.cancel()
252 for _, bg := range ts.background {
253 <-bg.done
254 }
255 ts.background = nil
256
257 markTime()
258
259 ts.t.Log("\n" + ts.abbrev(ts.log.String()))
260 }()
261
262
263 a, err := txtar.ParseFile(ts.file)
264 ts.check(err)
265 for _, f := range a.Files {
266 name := ts.mkabs(ts.expand(f.Name, false))
267 ts.check(os.MkdirAll(filepath.Dir(name), 0777))
268 ts.check(os.WriteFile(name, f.Data, 0666))
269 }
270
271
272 if *testWork || testing.Verbose() {
273
274 ts.cmdEnv(success, nil)
275 fmt.Fprintf(&ts.log, "\n")
276 ts.mark = ts.log.Len()
277 }
278
279
280
281 if *testSum != "" {
282 if ts.updateSum(a) {
283 defer func() {
284 if ts.t.Failed() {
285 return
286 }
287 data := txtar.Format(a)
288 if err := os.WriteFile(ts.file, data, 0666); err != nil {
289 ts.t.Errorf("rewriting test file: %v", err)
290 }
291 }()
292 }
293 }
294
295
296
297 script := string(a.Comment)
298 Script:
299 for script != "" {
300
301 ts.lineno++
302 var line string
303 if i := strings.Index(script, "\n"); i >= 0 {
304 line, script = script[:i], script[i+1:]
305 } else {
306 line, script = script, ""
307 }
308
309
310 if strings.HasPrefix(line, "#") {
311
312
313
314
315
316 if ts.log.Len() > ts.mark {
317 rewind()
318 markTime()
319 }
320
321 fmt.Fprintf(&ts.log, "%s\n", line)
322 ts.mark = ts.log.Len()
323 ts.start = time.Now()
324 continue
325 }
326
327
328 parsed := ts.parse(line)
329 if parsed.name == "" {
330 if parsed.want != "" || len(parsed.conds) > 0 {
331 ts.fatalf("missing command")
332 }
333 continue
334 }
335
336
337 fmt.Fprintf(&ts.log, "> %s\n", line)
338
339 for _, cond := range parsed.conds {
340 if err := ts.ctx.Err(); err != nil {
341 ts.fatalf("test interrupted: %v", err)
342 }
343
344
345
346
347
348 ok := false
349 switch cond.tag {
350 case runtime.GOOS, runtime.GOARCH, runtime.Compiler:
351 ok = true
352 case "short":
353 ok = testing.Short()
354 case "cgo":
355 ok = canCgo
356 case "msan":
357 ok = canMSan
358 case "asan":
359 ok = canASan
360 case "race":
361 ok = canRace
362 case "fuzz":
363 ok = canFuzz
364 case "fuzz-instrumented":
365 ok = fuzzInstrumented
366 case "net":
367 ok = testenv.HasExternalNetwork()
368 case "link":
369 ok = testenv.HasLink()
370 case "root":
371 ok = os.Geteuid() == 0
372 case "symlink":
373 ok = testenv.HasSymlink()
374 case "case-sensitive":
375 ok = isCaseSensitive(ts.t)
376 default:
377 if strings.HasPrefix(cond.tag, "exec:") {
378 prog := cond.tag[len("exec:"):]
379 ok = execCache.Do(prog, func() any {
380 if runtime.GOOS == "plan9" && prog == "git" {
381
382
383 return false
384 }
385 _, err := exec.LookPath(prog)
386 return err == nil
387 }).(bool)
388 break
389 }
390 if strings.HasPrefix(cond.tag, "GODEBUG:") {
391 value := strings.TrimPrefix(cond.tag, "GODEBUG:")
392 parts := strings.Split(os.Getenv("GODEBUG"), ",")
393 for _, p := range parts {
394 if strings.TrimSpace(p) == value {
395 ok = true
396 break
397 }
398 }
399 break
400 }
401 if strings.HasPrefix(cond.tag, "buildmode:") {
402 value := strings.TrimPrefix(cond.tag, "buildmode:")
403 ok = sys.BuildModeSupported(runtime.Compiler, value, runtime.GOOS, runtime.GOARCH)
404 break
405 }
406 if !imports.KnownArch[cond.tag] && !imports.KnownOS[cond.tag] && cond.tag != "gc" && cond.tag != "gccgo" {
407 ts.fatalf("unknown condition %q", cond.tag)
408 }
409 }
410 if ok != cond.want {
411
412 continue Script
413 }
414 }
415
416
417 cmd := scriptCmds[parsed.name]
418 if cmd == nil {
419 ts.fatalf("unknown command %q", parsed.name)
420 }
421 cmd(ts, parsed.want, parsed.args)
422
423
424 if ts.stopped {
425
426
427 break
428 }
429 }
430
431 ts.cancel()
432 ts.cmdWait(success, nil)
433
434
435 rewind()
436 markTime()
437 if !ts.stopped {
438 fmt.Fprintf(&ts.log, "PASS\n")
439 }
440 }
441
442 var (
443 onceCaseSensitive sync.Once
444 caseSensitive bool
445 )
446
447 func isCaseSensitive(t *testing.T) bool {
448 onceCaseSensitive.Do(func() {
449 tmpdir, err := os.MkdirTemp("", "case-sensitive")
450 if err != nil {
451 t.Fatal("failed to create directory to determine case-sensitivity:", err)
452 }
453 defer os.RemoveAll(tmpdir)
454
455 fcap := filepath.Join(tmpdir, "FILE")
456 if err := os.WriteFile(fcap, []byte{}, 0644); err != nil {
457 t.Fatal("error writing file to determine case-sensitivity:", err)
458 }
459
460 flow := filepath.Join(tmpdir, "file")
461 _, err = os.ReadFile(flow)
462 switch {
463 case err == nil:
464 caseSensitive = false
465 return
466 case os.IsNotExist(err):
467 caseSensitive = true
468 return
469 default:
470 t.Fatal("unexpected error reading file when determining case-sensitivity:", err)
471 }
472 })
473
474 return caseSensitive
475 }
476
477
478
479
480
481
482 var scriptCmds = map[string]func(*testScript, simpleStatus, []string){
483 "addcrlf": (*testScript).cmdAddcrlf,
484 "cc": (*testScript).cmdCc,
485 "cd": (*testScript).cmdCd,
486 "chmod": (*testScript).cmdChmod,
487 "cmp": (*testScript).cmdCmp,
488 "cmpenv": (*testScript).cmdCmpenv,
489 "cp": (*testScript).cmdCp,
490 "env": (*testScript).cmdEnv,
491 "exec": (*testScript).cmdExec,
492 "exists": (*testScript).cmdExists,
493 "go": (*testScript).cmdGo,
494 "grep": (*testScript).cmdGrep,
495 "mkdir": (*testScript).cmdMkdir,
496 "mv": (*testScript).cmdMv,
497 "rm": (*testScript).cmdRm,
498 "skip": (*testScript).cmdSkip,
499 "stale": (*testScript).cmdStale,
500 "stderr": (*testScript).cmdStderr,
501 "stdout": (*testScript).cmdStdout,
502 "stop": (*testScript).cmdStop,
503 "symlink": (*testScript).cmdSymlink,
504 "wait": (*testScript).cmdWait,
505 }
506
507
508
509 var regexpCmd = map[string]bool{
510 "grep": true,
511 "stderr": true,
512 "stdout": true,
513 }
514
515
516 func (ts *testScript) cmdAddcrlf(want simpleStatus, args []string) {
517 if len(args) == 0 {
518 ts.fatalf("usage: addcrlf file...")
519 }
520
521 for _, file := range args {
522 file = ts.mkabs(file)
523 data, err := os.ReadFile(file)
524 ts.check(err)
525 ts.check(os.WriteFile(file, bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")), 0666))
526 }
527 }
528
529
530 func (ts *testScript) cmdCc(want simpleStatus, args []string) {
531 if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
532 ts.fatalf("usage: cc args... [&]")
533 }
534
535 var b work.Builder
536 b.Init()
537 ts.cmdExec(want, append(b.GccCmd(".", ""), args...))
538 robustio.RemoveAll(b.WorkDir)
539 }
540
541
542 func (ts *testScript) cmdCd(want simpleStatus, args []string) {
543 if want != success {
544 ts.fatalf("unsupported: %v cd", want)
545 }
546 if len(args) != 1 {
547 ts.fatalf("usage: cd dir")
548 }
549
550 dir := filepath.FromSlash(args[0])
551 if !filepath.IsAbs(dir) {
552 dir = filepath.Join(ts.cd, dir)
553 }
554 info, err := os.Stat(dir)
555 if os.IsNotExist(err) {
556 ts.fatalf("directory %s does not exist", dir)
557 }
558 ts.check(err)
559 if !info.IsDir() {
560 ts.fatalf("%s is not a directory", dir)
561 }
562 ts.cd = dir
563 ts.envMap["PWD"] = dir
564 fmt.Fprintf(&ts.log, "%s\n", ts.cd)
565 }
566
567
568 func (ts *testScript) cmdChmod(want simpleStatus, args []string) {
569 if want != success {
570 ts.fatalf("unsupported: %v chmod", want)
571 }
572 if len(args) < 2 {
573 ts.fatalf("usage: chmod perm paths...")
574 }
575 perm, err := strconv.ParseUint(args[0], 0, 32)
576 if err != nil || perm&uint64(fs.ModePerm) != perm {
577 ts.fatalf("invalid mode: %s", args[0])
578 }
579 for _, arg := range args[1:] {
580 path := arg
581 if !filepath.IsAbs(path) {
582 path = filepath.Join(ts.cd, arg)
583 }
584 err := os.Chmod(path, fs.FileMode(perm))
585 ts.check(err)
586 }
587 }
588
589
590 func (ts *testScript) cmdCmp(want simpleStatus, args []string) {
591 quiet := false
592 if len(args) > 0 && args[0] == "-q" {
593 quiet = true
594 args = args[1:]
595 }
596 if len(args) != 2 {
597 ts.fatalf("usage: cmp file1 file2")
598 }
599 ts.doCmdCmp(want, args, false, quiet)
600 }
601
602
603 func (ts *testScript) cmdCmpenv(want simpleStatus, args []string) {
604 quiet := false
605 if len(args) > 0 && args[0] == "-q" {
606 quiet = true
607 args = args[1:]
608 }
609 if len(args) != 2 {
610 ts.fatalf("usage: cmpenv file1 file2")
611 }
612 ts.doCmdCmp(want, args, true, quiet)
613 }
614
615 func (ts *testScript) doCmdCmp(want simpleStatus, args []string, env, quiet bool) {
616 name1, name2 := args[0], args[1]
617 var text1, text2 string
618 switch name1 {
619 case "stdout":
620 text1 = ts.stdout
621 case "stderr":
622 text1 = ts.stderr
623 default:
624 data, err := os.ReadFile(ts.mkabs(name1))
625 ts.check(err)
626 text1 = string(data)
627 }
628
629 data, err := os.ReadFile(ts.mkabs(name2))
630 ts.check(err)
631 text2 = string(data)
632
633 if env {
634 text1 = ts.expand(text1, false)
635 text2 = ts.expand(text2, false)
636 }
637
638 eq := text1 == text2
639 if !eq && !quiet && want != failure {
640 fmt.Fprintf(&ts.log, "[diff -%s +%s]\n%s\n", name1, name2, diff(text1, text2))
641 }
642 switch want {
643 case failure:
644 if eq {
645 ts.fatalf("%s and %s do not differ", name1, name2)
646 }
647 case success:
648 if !eq {
649 ts.fatalf("%s and %s differ", name1, name2)
650 }
651 case successOrFailure:
652 if eq {
653 fmt.Fprintf(&ts.log, "%s and %s do not differ\n", name1, name2)
654 } else {
655 fmt.Fprintf(&ts.log, "%s and %s differ\n", name1, name2)
656 }
657 default:
658 ts.fatalf("unsupported: %v cmp", want)
659 }
660 }
661
662
663 func (ts *testScript) cmdCp(want simpleStatus, args []string) {
664 if len(args) < 2 {
665 ts.fatalf("usage: cp src... dst")
666 }
667
668 dst := ts.mkabs(args[len(args)-1])
669 info, err := os.Stat(dst)
670 dstDir := err == nil && info.IsDir()
671 if len(args) > 2 && !dstDir {
672 ts.fatalf("cp: destination %s is not a directory", dst)
673 }
674
675 for _, arg := range args[:len(args)-1] {
676 var (
677 src string
678 data []byte
679 mode fs.FileMode
680 )
681 switch arg {
682 case "stdout":
683 src = arg
684 data = []byte(ts.stdout)
685 mode = 0666
686 case "stderr":
687 src = arg
688 data = []byte(ts.stderr)
689 mode = 0666
690 default:
691 src = ts.mkabs(arg)
692 info, err := os.Stat(src)
693 ts.check(err)
694 mode = info.Mode() & 0777
695 data, err = os.ReadFile(src)
696 ts.check(err)
697 }
698 targ := dst
699 if dstDir {
700 targ = filepath.Join(dst, filepath.Base(src))
701 }
702 err := os.WriteFile(targ, data, mode)
703 switch want {
704 case failure:
705 if err == nil {
706 ts.fatalf("unexpected command success")
707 }
708 case success:
709 ts.check(err)
710 }
711 }
712 }
713
714
715 func (ts *testScript) cmdEnv(want simpleStatus, args []string) {
716 if want != success {
717 ts.fatalf("unsupported: %v env", want)
718 }
719
720 conv := func(s string) string { return s }
721 if len(args) > 0 && args[0] == "-r" {
722 conv = regexp.QuoteMeta
723 args = args[1:]
724 }
725
726 var out strings.Builder
727 if len(args) == 0 {
728 printed := make(map[string]bool)
729 for _, kv := range ts.env {
730 k := kv[:strings.Index(kv, "=")]
731 if !printed[k] {
732 fmt.Fprintf(&out, "%s=%s\n", k, ts.envMap[k])
733 }
734 }
735 } else {
736 for _, env := range args {
737 i := strings.Index(env, "=")
738 if i < 0 {
739
740 fmt.Fprintf(&out, "%s=%s\n", env, ts.envMap[env])
741 continue
742 }
743 key, val := env[:i], conv(env[i+1:])
744 ts.env = append(ts.env, key+"="+val)
745 ts.envMap[key] = val
746 }
747 }
748 if out.Len() > 0 || len(args) > 0 {
749 ts.stdout = out.String()
750 ts.log.WriteString(out.String())
751 }
752 }
753
754
755 func (ts *testScript) cmdExec(want simpleStatus, args []string) {
756 if len(args) < 1 || (len(args) == 1 && args[0] == "&") {
757 ts.fatalf("usage: exec program [args...] [&]")
758 }
759
760 background := false
761 if len(args) > 0 && args[len(args)-1] == "&" {
762 background = true
763 args = args[:len(args)-1]
764 }
765
766 bg, err := ts.startBackground(want, args[0], args[1:]...)
767 if err != nil {
768 ts.fatalf("unexpected error starting command: %v", err)
769 }
770 if background {
771 ts.stdout, ts.stderr = "", ""
772 ts.background = append(ts.background, bg)
773 return
774 }
775
776 <-bg.done
777 ts.stdout = bg.stdout.String()
778 ts.stderr = bg.stderr.String()
779 if ts.stdout != "" {
780 fmt.Fprintf(&ts.log, "[stdout]\n%s", ts.stdout)
781 }
782 if ts.stderr != "" {
783 fmt.Fprintf(&ts.log, "[stderr]\n%s", ts.stderr)
784 }
785 if bg.err != nil {
786 fmt.Fprintf(&ts.log, "[%v]\n", bg.err)
787 }
788 ts.checkCmd(bg)
789 }
790
791
792 func (ts *testScript) cmdExists(want simpleStatus, args []string) {
793 if want == successOrFailure {
794 ts.fatalf("unsupported: %v exists", want)
795 }
796 var readonly, exec bool
797 loop:
798 for len(args) > 0 {
799 switch args[0] {
800 case "-readonly":
801 readonly = true
802 args = args[1:]
803 case "-exec":
804 exec = true
805 args = args[1:]
806 default:
807 break loop
808 }
809 }
810 if len(args) == 0 {
811 ts.fatalf("usage: exists [-readonly] [-exec] file...")
812 }
813
814 for _, file := range args {
815 file = ts.mkabs(file)
816 info, err := os.Stat(file)
817 if err == nil && want == failure {
818 what := "file"
819 if info.IsDir() {
820 what = "directory"
821 }
822 ts.fatalf("%s %s unexpectedly exists", what, file)
823 }
824 if err != nil && want == success {
825 ts.fatalf("%s does not exist", file)
826 }
827 if err == nil && want == success && readonly && info.Mode()&0222 != 0 {
828 ts.fatalf("%s exists but is writable", file)
829 }
830 if err == nil && want == success && exec && runtime.GOOS != "windows" && info.Mode()&0111 == 0 {
831 ts.fatalf("%s exists but is not executable", file)
832 }
833 }
834 }
835
836
837 func (ts *testScript) cmdGo(want simpleStatus, args []string) {
838 ts.cmdExec(want, append([]string{testGo}, args...))
839 }
840
841
842 func (ts *testScript) cmdMkdir(want simpleStatus, args []string) {
843 if want != success {
844 ts.fatalf("unsupported: %v mkdir", want)
845 }
846 if len(args) < 1 {
847 ts.fatalf("usage: mkdir dir...")
848 }
849 for _, arg := range args {
850 ts.check(os.MkdirAll(ts.mkabs(arg), 0777))
851 }
852 }
853
854 func (ts *testScript) cmdMv(want simpleStatus, args []string) {
855 if want != success {
856 ts.fatalf("unsupported: %v mv", want)
857 }
858 if len(args) != 2 {
859 ts.fatalf("usage: mv old new")
860 }
861 ts.check(os.Rename(ts.mkabs(args[0]), ts.mkabs(args[1])))
862 }
863
864
865 func (ts *testScript) cmdRm(want simpleStatus, args []string) {
866 if want != success {
867 ts.fatalf("unsupported: %v rm", want)
868 }
869 if len(args) < 1 {
870 ts.fatalf("usage: rm file...")
871 }
872 for _, arg := range args {
873 file := ts.mkabs(arg)
874 removeAll(file)
875 ts.check(robustio.RemoveAll(file))
876 }
877 }
878
879
880 func (ts *testScript) cmdSkip(want simpleStatus, args []string) {
881 if len(args) > 1 {
882 ts.fatalf("usage: skip [msg]")
883 }
884 if want != success {
885 ts.fatalf("unsupported: %v skip", want)
886 }
887
888
889
890 ts.cancel()
891 ts.cmdWait(success, nil)
892
893 if len(args) == 1 {
894 ts.t.Skip(args[0])
895 }
896 ts.t.Skip()
897 }
898
899
900 func (ts *testScript) cmdStale(want simpleStatus, args []string) {
901 if len(args) == 0 {
902 ts.fatalf("usage: stale target...")
903 }
904 tmpl := "{{if .Error}}{{.ImportPath}}: {{.Error.Err}}{{else}}"
905 switch want {
906 case failure:
907 tmpl += "{{if .Stale}}{{.ImportPath}} is unexpectedly stale: {{.StaleReason}}{{end}}"
908 case success:
909 tmpl += "{{if not .Stale}}{{.ImportPath}} is unexpectedly NOT stale{{end}}"
910 default:
911 ts.fatalf("unsupported: %v stale", want)
912 }
913 tmpl += "{{end}}"
914 goArgs := append([]string{"list", "-e", "-f=" + tmpl}, args...)
915 stdout, stderr, err := ts.exec(testGo, goArgs...)
916 if err != nil {
917 ts.fatalf("go list: %v\n%s%s", err, stdout, stderr)
918 }
919 if stdout != "" {
920 ts.fatalf("%s", stdout)
921 }
922 }
923
924
925 func (ts *testScript) cmdStdout(want simpleStatus, args []string) {
926 scriptMatch(ts, want, args, ts.stdout, "stdout")
927 }
928
929
930 func (ts *testScript) cmdStderr(want simpleStatus, args []string) {
931 scriptMatch(ts, want, args, ts.stderr, "stderr")
932 }
933
934
935
936 func (ts *testScript) cmdGrep(want simpleStatus, args []string) {
937 scriptMatch(ts, want, args, "", "grep")
938 }
939
940
941 func scriptMatch(ts *testScript, want simpleStatus, args []string, text, name string) {
942 if want == successOrFailure {
943 ts.fatalf("unsupported: %v %s", want, name)
944 }
945
946 n := 0
947 if len(args) >= 1 && strings.HasPrefix(args[0], "-count=") {
948 if want == failure {
949 ts.fatalf("cannot use -count= with negated match")
950 }
951 var err error
952 n, err = strconv.Atoi(args[0][len("-count="):])
953 if err != nil {
954 ts.fatalf("bad -count=: %v", err)
955 }
956 if n < 1 {
957 ts.fatalf("bad -count=: must be at least 1")
958 }
959 args = args[1:]
960 }
961 quiet := false
962 if len(args) >= 1 && args[0] == "-q" {
963 quiet = true
964 args = args[1:]
965 }
966
967 extraUsage := ""
968 wantArgs := 1
969 if name == "grep" {
970 extraUsage = " file"
971 wantArgs = 2
972 }
973 if len(args) != wantArgs {
974 ts.fatalf("usage: %s [-count=N] 'pattern'%s", name, extraUsage)
975 }
976
977 pattern := `(?m)` + args[0]
978 re, err := regexp.Compile(pattern)
979 if err != nil {
980 ts.fatalf("regexp.Compile(%q): %v", pattern, err)
981 }
982
983 isGrep := name == "grep"
984 if isGrep {
985 name = args[1]
986 data, err := os.ReadFile(ts.mkabs(args[1]))
987 ts.check(err)
988 text = string(data)
989 }
990
991
992 text = strings.ReplaceAll(text, ts.workdir, "$WORK")
993
994 switch want {
995 case failure:
996 if re.MatchString(text) {
997 if isGrep && !quiet {
998 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
999 }
1000 ts.fatalf("unexpected match for %#q found in %s: %s", pattern, name, re.FindString(text))
1001 }
1002
1003 case success:
1004 if !re.MatchString(text) {
1005 if isGrep && !quiet {
1006 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
1007 }
1008 ts.fatalf("no match for %#q found in %s", pattern, name)
1009 }
1010 if n > 0 {
1011 count := len(re.FindAllString(text, -1))
1012 if count != n {
1013 if isGrep && !quiet {
1014 fmt.Fprintf(&ts.log, "[%s]\n%s\n", name, text)
1015 }
1016 ts.fatalf("have %d matches for %#q, want %d", count, pattern, n)
1017 }
1018 }
1019 }
1020 }
1021
1022
1023 func (ts *testScript) cmdStop(want simpleStatus, args []string) {
1024 if want != success {
1025 ts.fatalf("unsupported: %v stop", want)
1026 }
1027 if len(args) > 1 {
1028 ts.fatalf("usage: stop [msg]")
1029 }
1030 if len(args) == 1 {
1031 fmt.Fprintf(&ts.log, "stop: %s\n", args[0])
1032 } else {
1033 fmt.Fprintf(&ts.log, "stop\n")
1034 }
1035 ts.stopped = true
1036 }
1037
1038
1039 func (ts *testScript) cmdSymlink(want simpleStatus, args []string) {
1040 if want != success {
1041 ts.fatalf("unsupported: %v symlink", want)
1042 }
1043 if len(args) != 3 || args[1] != "->" {
1044 ts.fatalf("usage: symlink file -> target")
1045 }
1046
1047
1048 ts.check(os.Symlink(args[2], ts.mkabs(args[0])))
1049 }
1050
1051
1052 func (ts *testScript) cmdWait(want simpleStatus, args []string) {
1053 if want != success {
1054 ts.fatalf("unsupported: %v wait", want)
1055 }
1056 if len(args) > 0 {
1057 ts.fatalf("usage: wait")
1058 }
1059
1060 var stdouts, stderrs []string
1061 for _, bg := range ts.background {
1062 <-bg.done
1063
1064 args := append([]string{filepath.Base(bg.args[0])}, bg.args[1:]...)
1065 fmt.Fprintf(&ts.log, "[background] %s: %v\n", strings.Join(args, " "), bg.err)
1066
1067 cmdStdout := bg.stdout.String()
1068 if cmdStdout != "" {
1069 fmt.Fprintf(&ts.log, "[stdout]\n%s", cmdStdout)
1070 stdouts = append(stdouts, cmdStdout)
1071 }
1072
1073 cmdStderr := bg.stderr.String()
1074 if cmdStderr != "" {
1075 fmt.Fprintf(&ts.log, "[stderr]\n%s", cmdStderr)
1076 stderrs = append(stderrs, cmdStderr)
1077 }
1078
1079 ts.checkCmd(bg)
1080 }
1081
1082 ts.stdout = strings.Join(stdouts, "")
1083 ts.stderr = strings.Join(stderrs, "")
1084 ts.background = nil
1085 }
1086
1087
1088
1089
1090 func (ts *testScript) abbrev(s string) string {
1091 s = strings.ReplaceAll(s, ts.workdir, "$WORK")
1092 if *testWork {
1093
1094
1095 s = "WORK=" + ts.workdir + "\n" + strings.TrimPrefix(s, "WORK=$WORK\n")
1096 }
1097 return s
1098 }
1099
1100
1101 func (ts *testScript) check(err error) {
1102 if err != nil {
1103 ts.fatalf("%v", err)
1104 }
1105 }
1106
1107 func (ts *testScript) checkCmd(bg *backgroundCmd) {
1108 select {
1109 case <-bg.done:
1110 default:
1111 panic("checkCmd called when not done")
1112 }
1113
1114 if bg.err == nil {
1115 if bg.want == failure {
1116 ts.fatalf("unexpected command success")
1117 }
1118 return
1119 }
1120
1121 if errors.Is(bg.err, context.DeadlineExceeded) {
1122 ts.fatalf("test timed out while running command")
1123 }
1124
1125 if errors.Is(bg.err, context.Canceled) {
1126
1127
1128 if bg.want != successOrFailure {
1129 ts.fatalf("unexpected background command remaining at test end")
1130 }
1131 return
1132 }
1133
1134 if bg.want == success {
1135 ts.fatalf("unexpected command failure")
1136 }
1137 }
1138
1139
1140
1141 func (ts *testScript) exec(command string, args ...string) (stdout, stderr string, err error) {
1142 bg, err := ts.startBackground(success, command, args...)
1143 if err != nil {
1144 return "", "", err
1145 }
1146 <-bg.done
1147 return bg.stdout.String(), bg.stderr.String(), bg.err
1148 }
1149
1150
1151
1152 func (ts *testScript) startBackground(want simpleStatus, command string, args ...string) (*backgroundCmd, error) {
1153 done := make(chan struct{})
1154 bg := &backgroundCmd{
1155 want: want,
1156 args: append([]string{command}, args...),
1157 done: done,
1158 }
1159
1160
1161
1162
1163 command = filepath.FromSlash(command)
1164 if !strings.Contains(command, string(filepath.Separator)) {
1165 var err error
1166 command, err = ts.lookPath(command)
1167 if err != nil {
1168 return nil, err
1169 }
1170 }
1171 cmd := exec.Command(command, args...)
1172 cmd.Dir = ts.cd
1173 cmd.Env = append(ts.env, "PWD="+ts.cd)
1174 cmd.Stdout = &bg.stdout
1175 cmd.Stderr = &bg.stderr
1176 if err := cmd.Start(); err != nil {
1177 return nil, err
1178 }
1179
1180 go func() {
1181 bg.err = waitOrStop(ts.ctx, cmd, quitSignal(), ts.gracePeriod)
1182 close(done)
1183 }()
1184 return bg, nil
1185 }
1186
1187
1188
1189
1190 func (ts *testScript) lookPath(command string) (string, error) {
1191 var strEqual func(string, string) bool
1192 if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
1193
1194 strEqual = strings.EqualFold
1195 } else {
1196 strEqual = func(a, b string) bool { return a == b }
1197 }
1198
1199 var pathExt []string
1200 var searchExt bool
1201 var isExecutable func(os.FileInfo) bool
1202 if runtime.GOOS == "windows" {
1203
1204
1205
1206
1207
1208 pathExt = strings.Split(os.Getenv("PathExt"), string(filepath.ListSeparator))
1209 searchExt = true
1210 cmdExt := filepath.Ext(command)
1211 for _, ext := range pathExt {
1212 if strEqual(cmdExt, ext) {
1213 searchExt = false
1214 break
1215 }
1216 }
1217 isExecutable = func(fi os.FileInfo) bool {
1218 return fi.Mode().IsRegular()
1219 }
1220 } else {
1221 isExecutable = func(fi os.FileInfo) bool {
1222 return fi.Mode().IsRegular() && fi.Mode().Perm()&0111 != 0
1223 }
1224 }
1225
1226 pathName := "PATH"
1227 if runtime.GOOS == "plan9" {
1228 pathName = "path"
1229 }
1230
1231 for _, dir := range strings.Split(ts.envMap[pathName], string(filepath.ListSeparator)) {
1232 if searchExt {
1233 ents, err := os.ReadDir(dir)
1234 if err != nil {
1235 continue
1236 }
1237 for _, ent := range ents {
1238 for _, ext := range pathExt {
1239 if !ent.IsDir() && strEqual(ent.Name(), command+ext) {
1240 return dir + string(filepath.Separator) + ent.Name(), nil
1241 }
1242 }
1243 }
1244 } else {
1245 path := dir + string(filepath.Separator) + command
1246 if fi, err := os.Stat(path); err == nil && isExecutable(fi) {
1247 return path, nil
1248 }
1249 }
1250 }
1251 return "", &exec.Error{Name: command, Err: exec.ErrNotFound}
1252 }
1253
1254
1255
1256
1257
1258
1259
1260
1261 func waitOrStop(ctx context.Context, cmd *exec.Cmd, interrupt os.Signal, killDelay time.Duration) error {
1262 if cmd.Process == nil {
1263 panic("waitOrStop called with a nil cmd.Process — missing Start call?")
1264 }
1265 if interrupt == nil {
1266 panic("waitOrStop requires a non-nil interrupt signal")
1267 }
1268
1269 errc := make(chan error)
1270 go func() {
1271 select {
1272 case errc <- nil:
1273 return
1274 case <-ctx.Done():
1275 }
1276
1277 err := cmd.Process.Signal(interrupt)
1278 if err == nil {
1279 err = ctx.Err()
1280 } else if err == os.ErrProcessDone {
1281 errc <- nil
1282 return
1283 }
1284
1285 if killDelay > 0 {
1286 timer := time.NewTimer(killDelay)
1287 select {
1288
1289 case errc <- ctx.Err():
1290 timer.Stop()
1291 return
1292
1293 case <-timer.C:
1294 }
1295
1296
1297
1298
1299
1300
1301
1302 _ = cmd.Process.Kill()
1303 }
1304
1305 errc <- err
1306 }()
1307
1308 waitErr := cmd.Wait()
1309 if interruptErr := <-errc; interruptErr != nil {
1310 return interruptErr
1311 }
1312 return waitErr
1313 }
1314
1315
1316 func (ts *testScript) expand(s string, inRegexp bool) string {
1317 return os.Expand(s, func(key string) string {
1318 e := ts.envMap[key]
1319 if inRegexp {
1320
1321
1322 e = strings.ReplaceAll(e, ts.workdir, "$WORK")
1323
1324
1325
1326 e = regexp.QuoteMeta(e)
1327 }
1328 return e
1329 })
1330 }
1331
1332
1333 func (ts *testScript) fatalf(format string, args ...any) {
1334 fmt.Fprintf(&ts.log, "FAIL: %s:%d: %s\n", ts.file, ts.lineno, fmt.Sprintf(format, args...))
1335 ts.t.FailNow()
1336 }
1337
1338
1339
1340 func (ts *testScript) mkabs(file string) string {
1341 if filepath.IsAbs(file) {
1342 return file
1343 }
1344 return filepath.Join(ts.cd, file)
1345 }
1346
1347
1348 type condition struct {
1349 want bool
1350 tag string
1351 }
1352
1353
1354 type command struct {
1355 want simpleStatus
1356 conds []condition
1357 name string
1358 args []string
1359 }
1360
1361
1362
1363
1364
1365 func (ts *testScript) parse(line string) command {
1366 ts.line = line
1367
1368 var (
1369 cmd command
1370 arg string
1371 start = -1
1372 quoted = false
1373 isRegexp = false
1374 )
1375
1376 flushArg := func() {
1377 defer func() {
1378 arg = ""
1379 start = -1
1380 }()
1381
1382 if cmd.name != "" {
1383 cmd.args = append(cmd.args, arg)
1384
1385
1386
1387 if len(arg) == 0 || arg[0] != '-' {
1388 isRegexp = false
1389 }
1390 return
1391 }
1392
1393
1394
1395
1396 switch want := simpleStatus(arg); want {
1397 case failure, successOrFailure:
1398 if cmd.want != "" {
1399 ts.fatalf("duplicated '!' or '?' token")
1400 }
1401 cmd.want = want
1402 return
1403 }
1404
1405
1406 if strings.HasPrefix(arg, "[") && strings.HasSuffix(arg, "]") {
1407 want := true
1408 arg = strings.TrimSpace(arg[1 : len(arg)-1])
1409 if strings.HasPrefix(arg, "!") {
1410 want = false
1411 arg = strings.TrimSpace(arg[1:])
1412 }
1413 if arg == "" {
1414 ts.fatalf("empty condition")
1415 }
1416 cmd.conds = append(cmd.conds, condition{want: want, tag: arg})
1417 return
1418 }
1419
1420 cmd.name = arg
1421 isRegexp = regexpCmd[cmd.name]
1422 }
1423
1424 for i := 0; ; i++ {
1425 if !quoted && (i >= len(line) || line[i] == ' ' || line[i] == '\t' || line[i] == '\r' || line[i] == '#') {
1426
1427 if start >= 0 {
1428 arg += ts.expand(line[start:i], isRegexp)
1429 flushArg()
1430 }
1431 if i >= len(line) || line[i] == '#' {
1432 break
1433 }
1434 continue
1435 }
1436 if i >= len(line) {
1437 ts.fatalf("unterminated quoted argument")
1438 }
1439 if line[i] == '\'' {
1440 if !quoted {
1441
1442 if start >= 0 {
1443 arg += ts.expand(line[start:i], isRegexp)
1444 }
1445 start = i + 1
1446 quoted = true
1447 continue
1448 }
1449
1450 if i+1 < len(line) && line[i+1] == '\'' {
1451 arg += line[start:i]
1452 start = i + 1
1453 i++
1454 continue
1455 }
1456
1457 arg += line[start:i]
1458 start = i + 1
1459 quoted = false
1460 continue
1461 }
1462
1463 if start < 0 {
1464 start = i
1465 }
1466 }
1467 return cmd
1468 }
1469
1470
1471
1472
1473
1474 func (ts *testScript) updateSum(archive *txtar.Archive) (rewrite bool) {
1475 gomodIdx, gosumIdx := -1, -1
1476 for i := range archive.Files {
1477 switch archive.Files[i].Name {
1478 case "go.mod":
1479 gomodIdx = i
1480 case "go.sum":
1481 gosumIdx = i
1482 }
1483 }
1484 if gomodIdx < 0 {
1485 return false
1486 }
1487
1488 switch *testSum {
1489 case "tidy":
1490 ts.cmdGo(success, []string{"mod", "tidy"})
1491 case "listm":
1492 ts.cmdGo(success, []string{"list", "-m", "-mod=mod", "all"})
1493 case "listall":
1494 ts.cmdGo(success, []string{"list", "-mod=mod", "all"})
1495 default:
1496 ts.t.Fatalf(`unknown value for -testsum %q; may be "tidy", "listm", or "listall"`, *testSum)
1497 }
1498
1499 newGomodData, err := os.ReadFile(filepath.Join(ts.cd, "go.mod"))
1500 if err != nil {
1501 ts.t.Fatalf("reading go.mod after -testsum: %v", err)
1502 }
1503 if !bytes.Equal(newGomodData, archive.Files[gomodIdx].Data) {
1504 archive.Files[gomodIdx].Data = newGomodData
1505 rewrite = true
1506 }
1507
1508 newGosumData, err := os.ReadFile(filepath.Join(ts.cd, "go.sum"))
1509 if err != nil && !os.IsNotExist(err) {
1510 ts.t.Fatalf("reading go.sum after -testsum: %v", err)
1511 }
1512 switch {
1513 case os.IsNotExist(err) && gosumIdx >= 0:
1514
1515 rewrite = true
1516 archive.Files = append(archive.Files[:gosumIdx], archive.Files[gosumIdx+1:]...)
1517 case err == nil && gosumIdx < 0:
1518
1519 rewrite = true
1520 gosumIdx = gomodIdx + 1
1521 archive.Files = append(archive.Files, txtar.File{})
1522 copy(archive.Files[gosumIdx+1:], archive.Files[gosumIdx:])
1523 archive.Files[gosumIdx] = txtar.File{Name: "go.sum", Data: newGosumData}
1524 case err == nil && gosumIdx >= 0 && !bytes.Equal(newGosumData, archive.Files[gosumIdx].Data):
1525
1526 rewrite = true
1527 archive.Files[gosumIdx].Data = newGosumData
1528 }
1529 return rewrite
1530 }
1531
1532
1533
1534
1535
1536
1537 func diff(text1, text2 string) string {
1538 if text1 != "" && !strings.HasSuffix(text1, "\n") {
1539 text1 += "(missing final newline)"
1540 }
1541 lines1 := strings.Split(text1, "\n")
1542 lines1 = lines1[:len(lines1)-1]
1543 if text2 != "" && !strings.HasSuffix(text2, "\n") {
1544 text2 += "(missing final newline)"
1545 }
1546 lines2 := strings.Split(text2, "\n")
1547 lines2 = lines2[:len(lines2)-1]
1548
1549
1550
1551
1552
1553
1554 dist := make([][]int, len(lines1)+1)
1555 for i := range dist {
1556 dist[i] = make([]int, len(lines2)+1)
1557 if i == 0 {
1558 for j := range dist[0] {
1559 dist[0][j] = j
1560 }
1561 continue
1562 }
1563 for j := range dist[i] {
1564 if j == 0 {
1565 dist[i][0] = i
1566 continue
1567 }
1568 cost := dist[i][j-1] + 1
1569 if cost > dist[i-1][j]+1 {
1570 cost = dist[i-1][j] + 1
1571 }
1572 if lines1[len(lines1)-i] == lines2[len(lines2)-j] {
1573 if cost > dist[i-1][j-1] {
1574 cost = dist[i-1][j-1]
1575 }
1576 }
1577 dist[i][j] = cost
1578 }
1579 }
1580
1581 var buf strings.Builder
1582 i, j := len(lines1), len(lines2)
1583 for i > 0 || j > 0 {
1584 cost := dist[i][j]
1585 if i > 0 && j > 0 && cost == dist[i-1][j-1] && lines1[len(lines1)-i] == lines2[len(lines2)-j] {
1586 fmt.Fprintf(&buf, " %s\n", lines1[len(lines1)-i])
1587 i--
1588 j--
1589 } else if i > 0 && cost == dist[i-1][j]+1 {
1590 fmt.Fprintf(&buf, "-%s\n", lines1[len(lines1)-i])
1591 i--
1592 } else {
1593 fmt.Fprintf(&buf, "+%s\n", lines2[len(lines2)-j])
1594 j--
1595 }
1596 }
1597 return buf.String()
1598 }
1599
1600 var diffTests = []struct {
1601 text1 string
1602 text2 string
1603 diff string
1604 }{
1605 {"a b c", "a b d e f", "a b -c +d +e +f"},
1606 {"", "a b c", "+a +b +c"},
1607 {"a b c", "", "-a -b -c"},
1608 {"a b c", "d e f", "-a -b -c +d +e +f"},
1609 {"a b c d e f", "a b d e f", "a b -c d e f"},
1610 {"a b c e f", "a b c d e f", "a b c +d e f"},
1611 }
1612
1613 func TestDiff(t *testing.T) {
1614 t.Parallel()
1615
1616 for _, tt := range diffTests {
1617
1618 text1 := strings.ReplaceAll(tt.text1, " ", "\n")
1619 if text1 != "" {
1620 text1 += "\n"
1621 }
1622 text2 := strings.ReplaceAll(tt.text2, " ", "\n")
1623 if text2 != "" {
1624 text2 += "\n"
1625 }
1626 out := diff(text1, text2)
1627
1628 out = strings.ReplaceAll(strings.ReplaceAll(strings.TrimSuffix(out, "\n"), " ", ""), "\n", " ")
1629 if out != tt.diff {
1630 t.Errorf("diff(%q, %q) = %q, want %q", text1, text2, out, tt.diff)
1631 }
1632 }
1633 }
1634
View as plain text