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