Source file
src/os/exec/exec_test.go
1
2
3
4
5
6
7
8 package exec_test
9
10 import (
11 "bufio"
12 "bytes"
13 "context"
14 "errors"
15 "flag"
16 "fmt"
17 "internal/poll"
18 "internal/testenv"
19 "io"
20 "log"
21 "net"
22 "net/http"
23 "net/http/httptest"
24 "os"
25 "os/exec"
26 "os/exec/internal/fdtest"
27 "os/signal"
28 "path/filepath"
29 "runtime"
30 "runtime/debug"
31 "strconv"
32 "strings"
33 "sync"
34 "sync/atomic"
35 "testing"
36 "time"
37 )
38
39
40
41 var haveUnexpectedFDs bool
42
43 func init() {
44 godebug := os.Getenv("GODEBUG")
45 if godebug != "" {
46 godebug += ","
47 }
48 godebug += "execwait=2"
49 os.Setenv("GODEBUG", godebug)
50
51 if os.Getenv("GO_EXEC_TEST_PID") != "" {
52 return
53 }
54 if runtime.GOOS == "windows" {
55 return
56 }
57 for fd := uintptr(3); fd <= 100; fd++ {
58 if poll.IsPollDescriptor(fd) {
59 continue
60 }
61
62 if fdtest.Exists(fd) {
63 haveUnexpectedFDs = true
64 return
65 }
66 }
67 }
68
69
70
71
72
73 func TestMain(m *testing.M) {
74 flag.Parse()
75
76 pid := os.Getpid()
77 if os.Getenv("GO_EXEC_TEST_PID") == "" {
78 os.Setenv("GO_EXEC_TEST_PID", strconv.Itoa(pid))
79
80 code := m.Run()
81 if code == 0 && flag.Lookup("test.run").Value.String() == "" && flag.Lookup("test.list").Value.String() == "" {
82 for cmd := range helperCommands {
83 if _, ok := helperCommandUsed.Load(cmd); !ok {
84 fmt.Fprintf(os.Stderr, "helper command unused: %q\n", cmd)
85 code = 1
86 }
87 }
88 }
89
90 if !testing.Short() {
91
92
93 runtime.GC()
94 runtime.GC()
95 }
96
97 os.Exit(code)
98 }
99
100 args := flag.Args()
101 if len(args) == 0 {
102 fmt.Fprintf(os.Stderr, "No command\n")
103 os.Exit(2)
104 }
105
106 cmd, args := args[0], args[1:]
107 f, ok := helperCommands[cmd]
108 if !ok {
109 fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd)
110 os.Exit(2)
111 }
112 f(args...)
113 os.Exit(0)
114 }
115
116
117
118
119
120
121 func registerHelperCommand(name string, f func(...string)) {
122 if helperCommands[name] != nil {
123 panic("duplicate command registered: " + name)
124 }
125 helperCommands[name] = f
126 }
127
128
129
130
131 func maySkipHelperCommand(name string) {
132 helperCommandUsed.Store(name, true)
133 }
134
135
136 func helperCommand(t *testing.T, name string, args ...string) *exec.Cmd {
137 t.Helper()
138 return helperCommandContext(t, nil, name, args...)
139 }
140
141
142
143 func helperCommandContext(t *testing.T, ctx context.Context, name string, args ...string) (cmd *exec.Cmd) {
144 helperCommandUsed.LoadOrStore(name, true)
145
146 t.Helper()
147 testenv.MustHaveExec(t)
148
149 cs := append([]string{name}, args...)
150 if ctx != nil {
151 cmd = exec.CommandContext(ctx, exePath(t), cs...)
152 } else {
153 cmd = exec.Command(exePath(t), cs...)
154 }
155 return cmd
156 }
157
158
159 func exePath(t testing.TB) string {
160 exeOnce.Do(func() {
161
162
163
164 exeOnce.path, exeOnce.err = os.Executable()
165 })
166
167 if exeOnce.err != nil {
168 if t == nil {
169 panic(exeOnce.err)
170 }
171 t.Fatal(exeOnce.err)
172 }
173
174 return exeOnce.path
175 }
176
177 var exeOnce struct {
178 path string
179 err error
180 sync.Once
181 }
182
183 var helperCommandUsed sync.Map
184
185 var helperCommands = map[string]func(...string){
186 "echo": cmdEcho,
187 "echoenv": cmdEchoEnv,
188 "cat": cmdCat,
189 "pipetest": cmdPipeTest,
190 "stdinClose": cmdStdinClose,
191 "exit": cmdExit,
192 "describefiles": cmdDescribeFiles,
193 "stderrfail": cmdStderrFail,
194 "yes": cmdYes,
195 "hang": cmdHang,
196 }
197
198 func cmdEcho(args ...string) {
199 iargs := []any{}
200 for _, s := range args {
201 iargs = append(iargs, s)
202 }
203 fmt.Println(iargs...)
204 }
205
206 func cmdEchoEnv(args ...string) {
207 for _, s := range args {
208 fmt.Println(os.Getenv(s))
209 }
210 }
211
212 func cmdCat(args ...string) {
213 if len(args) == 0 {
214 io.Copy(os.Stdout, os.Stdin)
215 return
216 }
217 exit := 0
218 for _, fn := range args {
219 f, err := os.Open(fn)
220 if err != nil {
221 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
222 exit = 2
223 } else {
224 defer f.Close()
225 io.Copy(os.Stdout, f)
226 }
227 }
228 os.Exit(exit)
229 }
230
231 func cmdPipeTest(...string) {
232 bufr := bufio.NewReader(os.Stdin)
233 for {
234 line, _, err := bufr.ReadLine()
235 if err == io.EOF {
236 break
237 } else if err != nil {
238 os.Exit(1)
239 }
240 if bytes.HasPrefix(line, []byte("O:")) {
241 os.Stdout.Write(line)
242 os.Stdout.Write([]byte{'\n'})
243 } else if bytes.HasPrefix(line, []byte("E:")) {
244 os.Stderr.Write(line)
245 os.Stderr.Write([]byte{'\n'})
246 } else {
247 os.Exit(1)
248 }
249 }
250 }
251
252 func cmdStdinClose(...string) {
253 b, err := io.ReadAll(os.Stdin)
254 if err != nil {
255 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
256 os.Exit(1)
257 }
258 if s := string(b); s != stdinCloseTestString {
259 fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString)
260 os.Exit(1)
261 }
262 }
263
264 func cmdExit(args ...string) {
265 n, _ := strconv.Atoi(args[0])
266 os.Exit(n)
267 }
268
269 func cmdDescribeFiles(args ...string) {
270 f := os.NewFile(3, fmt.Sprintf("fd3"))
271 ln, err := net.FileListener(f)
272 if err == nil {
273 fmt.Printf("fd3: listener %s\n", ln.Addr())
274 ln.Close()
275 }
276 }
277
278 func cmdStderrFail(...string) {
279 fmt.Fprintf(os.Stderr, "some stderr text\n")
280 os.Exit(1)
281 }
282
283 func cmdYes(args ...string) {
284 if len(args) == 0 {
285 args = []string{"y"}
286 }
287 s := strings.Join(args, " ") + "\n"
288 for {
289 _, err := os.Stdout.WriteString(s)
290 if err != nil {
291 os.Exit(1)
292 }
293 }
294 }
295
296 func TestEcho(t *testing.T) {
297 t.Parallel()
298
299 bs, err := helperCommand(t, "echo", "foo bar", "baz").Output()
300 if err != nil {
301 t.Errorf("echo: %v", err)
302 }
303 if g, e := string(bs), "foo bar baz\n"; g != e {
304 t.Errorf("echo: want %q, got %q", e, g)
305 }
306 }
307
308 func TestCommandRelativeName(t *testing.T) {
309 t.Parallel()
310
311 cmd := helperCommand(t, "echo", "foo")
312
313
314
315 base := filepath.Base(os.Args[0])
316 dir := filepath.Dir(os.Args[0])
317 if dir == "." {
318 t.Skip("skipping; running test at root somehow")
319 }
320 parentDir := filepath.Dir(dir)
321 dirBase := filepath.Base(dir)
322 if dirBase == "." {
323 t.Skipf("skipping; unexpected shallow dir of %q", dir)
324 }
325
326 cmd.Path = filepath.Join(dirBase, base)
327 cmd.Dir = parentDir
328
329 out, err := cmd.Output()
330 if err != nil {
331 t.Errorf("echo: %v", err)
332 }
333 if g, e := string(out), "foo\n"; g != e {
334 t.Errorf("echo: want %q, got %q", e, g)
335 }
336 }
337
338 func TestCatStdin(t *testing.T) {
339 t.Parallel()
340
341
342 input := "Input string\nLine 2"
343 p := helperCommand(t, "cat")
344 p.Stdin = strings.NewReader(input)
345 bs, err := p.Output()
346 if err != nil {
347 t.Errorf("cat: %v", err)
348 }
349 s := string(bs)
350 if s != input {
351 t.Errorf("cat: want %q, got %q", input, s)
352 }
353 }
354
355 func TestEchoFileRace(t *testing.T) {
356 t.Parallel()
357
358 cmd := helperCommand(t, "echo")
359 stdin, err := cmd.StdinPipe()
360 if err != nil {
361 t.Fatalf("StdinPipe: %v", err)
362 }
363 if err := cmd.Start(); err != nil {
364 t.Fatalf("Start: %v", err)
365 }
366 wrote := make(chan bool)
367 go func() {
368 defer close(wrote)
369 fmt.Fprint(stdin, "echo\n")
370 }()
371 if err := cmd.Wait(); err != nil {
372 t.Fatalf("Wait: %v", err)
373 }
374 <-wrote
375 }
376
377 func TestCatGoodAndBadFile(t *testing.T) {
378 t.Parallel()
379
380
381 bs, err := helperCommand(t, "cat", "/bogus/file.foo", "exec_test.go").CombinedOutput()
382 if _, ok := err.(*exec.ExitError); !ok {
383 t.Errorf("expected *exec.ExitError from cat combined; got %T: %v", err, err)
384 }
385 errLine, body, ok := strings.Cut(string(bs), "\n")
386 if !ok {
387 t.Fatalf("expected two lines from cat; got %q", bs)
388 }
389 if !strings.HasPrefix(errLine, "Error: open /bogus/file.foo") {
390 t.Errorf("expected stderr to complain about file; got %q", errLine)
391 }
392 if !strings.Contains(body, "func TestCatGoodAndBadFile(t *testing.T)") {
393 t.Errorf("expected test code; got %q (len %d)", body, len(body))
394 }
395 }
396
397 func TestNoExistExecutable(t *testing.T) {
398 t.Parallel()
399
400
401 err := exec.Command("/no-exist-executable").Run()
402 if err == nil {
403 t.Error("expected error from /no-exist-executable")
404 }
405 }
406
407 func TestExitStatus(t *testing.T) {
408 t.Parallel()
409
410
411 cmd := helperCommand(t, "exit", "42")
412 err := cmd.Run()
413 want := "exit status 42"
414 switch runtime.GOOS {
415 case "plan9":
416 want = fmt.Sprintf("exit status: '%s %d: 42'", filepath.Base(cmd.Path), cmd.ProcessState.Pid())
417 }
418 if werr, ok := err.(*exec.ExitError); ok {
419 if s := werr.Error(); s != want {
420 t.Errorf("from exit 42 got exit %q, want %q", s, want)
421 }
422 } else {
423 t.Fatalf("expected *exec.ExitError from exit 42; got %T: %v", err, err)
424 }
425 }
426
427 func TestExitCode(t *testing.T) {
428 t.Parallel()
429
430
431 cmd := helperCommand(t, "exit", "42")
432 cmd.Run()
433 want := 42
434 if runtime.GOOS == "plan9" {
435 want = 1
436 }
437 got := cmd.ProcessState.ExitCode()
438 if want != got {
439 t.Errorf("ExitCode got %d, want %d", got, want)
440 }
441
442 cmd = helperCommand(t, "/no-exist-executable")
443 cmd.Run()
444 want = 2
445 if runtime.GOOS == "plan9" {
446 want = 1
447 }
448 got = cmd.ProcessState.ExitCode()
449 if want != got {
450 t.Errorf("ExitCode got %d, want %d", got, want)
451 }
452
453 cmd = helperCommand(t, "exit", "255")
454 cmd.Run()
455 want = 255
456 if runtime.GOOS == "plan9" {
457 want = 1
458 }
459 got = cmd.ProcessState.ExitCode()
460 if want != got {
461 t.Errorf("ExitCode got %d, want %d", got, want)
462 }
463
464 cmd = helperCommand(t, "cat")
465 cmd.Run()
466 want = 0
467 got = cmd.ProcessState.ExitCode()
468 if want != got {
469 t.Errorf("ExitCode got %d, want %d", got, want)
470 }
471
472
473 cmd = helperCommand(t, "cat")
474 want = -1
475 got = cmd.ProcessState.ExitCode()
476 if want != got {
477 t.Errorf("ExitCode got %d, want %d", got, want)
478 }
479 }
480
481 func TestPipes(t *testing.T) {
482 t.Parallel()
483
484 check := func(what string, err error) {
485 if err != nil {
486 t.Fatalf("%s: %v", what, err)
487 }
488 }
489
490 c := helperCommand(t, "pipetest")
491 stdin, err := c.StdinPipe()
492 check("StdinPipe", err)
493 stdout, err := c.StdoutPipe()
494 check("StdoutPipe", err)
495 stderr, err := c.StderrPipe()
496 check("StderrPipe", err)
497
498 outbr := bufio.NewReader(stdout)
499 errbr := bufio.NewReader(stderr)
500 line := func(what string, br *bufio.Reader) string {
501 line, _, err := br.ReadLine()
502 if err != nil {
503 t.Fatalf("%s: %v", what, err)
504 }
505 return string(line)
506 }
507
508 err = c.Start()
509 check("Start", err)
510
511 _, err = stdin.Write([]byte("O:I am output\n"))
512 check("first stdin Write", err)
513 if g, e := line("first output line", outbr), "O:I am output"; g != e {
514 t.Errorf("got %q, want %q", g, e)
515 }
516
517 _, err = stdin.Write([]byte("E:I am error\n"))
518 check("second stdin Write", err)
519 if g, e := line("first error line", errbr), "E:I am error"; g != e {
520 t.Errorf("got %q, want %q", g, e)
521 }
522
523 _, err = stdin.Write([]byte("O:I am output2\n"))
524 check("third stdin Write 3", err)
525 if g, e := line("second output line", outbr), "O:I am output2"; g != e {
526 t.Errorf("got %q, want %q", g, e)
527 }
528
529 stdin.Close()
530 err = c.Wait()
531 check("Wait", err)
532 }
533
534 const stdinCloseTestString = "Some test string."
535
536
537 func TestStdinClose(t *testing.T) {
538 t.Parallel()
539
540 check := func(what string, err error) {
541 if err != nil {
542 t.Fatalf("%s: %v", what, err)
543 }
544 }
545 cmd := helperCommand(t, "stdinClose")
546 stdin, err := cmd.StdinPipe()
547 check("StdinPipe", err)
548
549 if _, ok := stdin.(interface {
550 Fd() uintptr
551 }); !ok {
552 t.Error("can't access methods of underlying *os.File")
553 }
554 check("Start", cmd.Start())
555
556 var wg sync.WaitGroup
557 wg.Add(1)
558 defer wg.Wait()
559 go func() {
560 defer wg.Done()
561
562 _, err := io.Copy(stdin, strings.NewReader(stdinCloseTestString))
563 check("Copy", err)
564
565
566 if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
567 t.Errorf("Close: %v", err)
568 }
569 }()
570
571 check("Wait", cmd.Wait())
572 }
573
574
575
576
577
578
579
580 func TestStdinCloseRace(t *testing.T) {
581 t.Parallel()
582
583 cmd := helperCommand(t, "stdinClose")
584 stdin, err := cmd.StdinPipe()
585 if err != nil {
586 t.Fatalf("StdinPipe: %v", err)
587 }
588 if err := cmd.Start(); err != nil {
589 t.Fatalf("Start: %v", err)
590
591 }
592
593 var wg sync.WaitGroup
594 wg.Add(2)
595 defer wg.Wait()
596
597 go func() {
598 defer wg.Done()
599
600
601
602
603
604
605 cmd.Process.Kill()
606 }()
607
608 go func() {
609 defer wg.Done()
610
611
612
613
614
615 io.Copy(stdin, strings.NewReader("unexpected string"))
616 if err := stdin.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
617 t.Errorf("stdin.Close: %v", err)
618 }
619 }()
620
621 if err := cmd.Wait(); err == nil {
622 t.Fatalf("Wait: succeeded unexpectedly")
623 }
624 }
625
626
627 func TestPipeLookPathLeak(t *testing.T) {
628 if runtime.GOOS == "windows" {
629 t.Skip("we don't currently suppore counting open handles on windows")
630 }
631
632
633 openFDs := func() []uintptr {
634 var fds []uintptr
635 for i := uintptr(0); i < 100; i++ {
636 if fdtest.Exists(i) {
637 fds = append(fds, i)
638 }
639 }
640 return fds
641 }
642
643 old := map[uintptr]bool{}
644 for _, fd := range openFDs() {
645 old[fd] = true
646 }
647
648 for i := 0; i < 6; i++ {
649 cmd := exec.Command("something-that-does-not-exist-executable")
650 cmd.StdoutPipe()
651 cmd.StderrPipe()
652 cmd.StdinPipe()
653 if err := cmd.Run(); err == nil {
654 t.Fatal("unexpected success")
655 }
656 }
657
658
659
660
661
662
663 for _, fd := range openFDs() {
664 if !old[fd] {
665 t.Errorf("leaked file descriptor %v", fd)
666 }
667 }
668 }
669
670 func TestExtraFiles(t *testing.T) {
671 if testing.Short() {
672 t.Skipf("skipping test in short mode that would build a helper binary")
673 }
674
675 if haveUnexpectedFDs {
676
677
678
679
680
681
682
683
684
685
686
687
688
689 t.Skip("skipping test because test was run with FDs open")
690 }
691
692 testenv.MustHaveExec(t)
693 testenv.MustHaveGoBuild(t)
694
695
696
697 testenv.MustInternalLink(t)
698
699 if runtime.GOOS == "windows" {
700 t.Skipf("skipping test on %q", runtime.GOOS)
701 }
702
703
704
705 ln, err := net.Listen("tcp", "127.0.0.1:0")
706 if err != nil {
707 t.Fatal(err)
708 }
709 defer ln.Close()
710
711
712 f, err := ln.(*net.TCPListener).File()
713 if err != nil {
714 t.Fatal(err)
715 }
716 defer f.Close()
717 ln2, err := net.FileListener(f)
718 if err != nil {
719 t.Fatal(err)
720 }
721 defer ln2.Close()
722
723
724
725 ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
726
727 ts.Config.ErrorLog = log.New(io.Discard, "", 0)
728 ts.StartTLS()
729 defer ts.Close()
730 _, err = http.Get(ts.URL)
731 if err == nil {
732 t.Errorf("success trying to fetch %s; want an error", ts.URL)
733 }
734
735 tf, err := os.CreateTemp("", "")
736 if err != nil {
737 t.Fatalf("TempFile: %v", err)
738 }
739 defer os.Remove(tf.Name())
740 defer tf.Close()
741
742 const text = "Hello, fd 3!"
743 _, err = tf.Write([]byte(text))
744 if err != nil {
745 t.Fatalf("Write: %v", err)
746 }
747 _, err = tf.Seek(0, io.SeekStart)
748 if err != nil {
749 t.Fatalf("Seek: %v", err)
750 }
751
752 tempdir := t.TempDir()
753 exe := filepath.Join(tempdir, "read3.exe")
754
755 c := exec.Command(testenv.GoToolPath(t), "build", "-o", exe, "read3.go")
756
757
758 c.Env = append(os.Environ(), "CGO_ENABLED=0")
759 if output, err := c.CombinedOutput(); err != nil {
760 t.Logf("go build -o %s read3.go\n%s", exe, output)
761 t.Fatalf("go build failed: %v", err)
762 }
763
764
765 ctx := context.Background()
766 if deadline, ok := t.Deadline(); ok {
767
768
769 deadline = deadline.Add(-time.Until(deadline) / 5)
770
771 var cancel context.CancelFunc
772 ctx, cancel = context.WithDeadline(ctx, deadline)
773 defer cancel()
774 }
775
776 c = exec.CommandContext(ctx, exe)
777 var stdout, stderr strings.Builder
778 c.Stdout = &stdout
779 c.Stderr = &stderr
780 c.ExtraFiles = []*os.File{tf}
781 if runtime.GOOS == "illumos" {
782
783
784
785
786
787
788
789
790
791 c.Env = append(os.Environ(), "GOMAXPROCS=1")
792 }
793 err = c.Run()
794 if err != nil {
795 t.Fatalf("Run: %v\n--- stdout:\n%s--- stderr:\n%s", err, stdout.String(), stderr.String())
796 }
797 if stdout.String() != text {
798 t.Errorf("got stdout %q, stderr %q; want %q on stdout", stdout.String(), stderr.String(), text)
799 }
800 }
801
802 func TestExtraFilesRace(t *testing.T) {
803 if runtime.GOOS == "windows" {
804 maySkipHelperCommand("describefiles")
805 t.Skip("no operating system support; skipping")
806 }
807 t.Parallel()
808
809 listen := func() net.Listener {
810 ln, err := net.Listen("tcp", "127.0.0.1:0")
811 if err != nil {
812 t.Fatal(err)
813 }
814 return ln
815 }
816 listenerFile := func(ln net.Listener) *os.File {
817 f, err := ln.(*net.TCPListener).File()
818 if err != nil {
819 t.Fatal(err)
820 }
821 return f
822 }
823 runCommand := func(c *exec.Cmd, out chan<- string) {
824 bout, err := c.CombinedOutput()
825 if err != nil {
826 out <- "ERROR:" + err.Error()
827 } else {
828 out <- string(bout)
829 }
830 }
831
832 for i := 0; i < 10; i++ {
833 if testing.Short() && i >= 3 {
834 break
835 }
836 la := listen()
837 ca := helperCommand(t, "describefiles")
838 ca.ExtraFiles = []*os.File{listenerFile(la)}
839 lb := listen()
840 cb := helperCommand(t, "describefiles")
841 cb.ExtraFiles = []*os.File{listenerFile(lb)}
842 ares := make(chan string)
843 bres := make(chan string)
844 go runCommand(ca, ares)
845 go runCommand(cb, bres)
846 if got, want := <-ares, fmt.Sprintf("fd3: listener %s\n", la.Addr()); got != want {
847 t.Errorf("iteration %d, process A got:\n%s\nwant:\n%s\n", i, got, want)
848 }
849 if got, want := <-bres, fmt.Sprintf("fd3: listener %s\n", lb.Addr()); got != want {
850 t.Errorf("iteration %d, process B got:\n%s\nwant:\n%s\n", i, got, want)
851 }
852 la.Close()
853 lb.Close()
854 for _, f := range ca.ExtraFiles {
855 f.Close()
856 }
857 for _, f := range cb.ExtraFiles {
858 f.Close()
859 }
860 }
861 }
862
863 type delayedInfiniteReader struct{}
864
865 func (delayedInfiniteReader) Read(b []byte) (int, error) {
866 time.Sleep(100 * time.Millisecond)
867 for i := range b {
868 b[i] = 'x'
869 }
870 return len(b), nil
871 }
872
873
874 func TestIgnorePipeErrorOnSuccess(t *testing.T) {
875 t.Parallel()
876
877 testWith := func(r io.Reader) func(*testing.T) {
878 return func(t *testing.T) {
879 t.Parallel()
880
881 cmd := helperCommand(t, "echo", "foo")
882 var out strings.Builder
883 cmd.Stdin = r
884 cmd.Stdout = &out
885 if err := cmd.Run(); err != nil {
886 t.Fatal(err)
887 }
888 if got, want := out.String(), "foo\n"; got != want {
889 t.Errorf("output = %q; want %q", got, want)
890 }
891 }
892 }
893 t.Run("10MB", testWith(strings.NewReader(strings.Repeat("x", 10<<20))))
894 t.Run("Infinite", testWith(delayedInfiniteReader{}))
895 }
896
897 type badWriter struct{}
898
899 func (w *badWriter) Write(data []byte) (int, error) {
900 return 0, io.ErrUnexpectedEOF
901 }
902
903 func TestClosePipeOnCopyError(t *testing.T) {
904 t.Parallel()
905
906 cmd := helperCommand(t, "yes")
907 cmd.Stdout = new(badWriter)
908 err := cmd.Run()
909 if err == nil {
910 t.Errorf("yes unexpectedly completed successfully")
911 }
912 }
913
914 func TestOutputStderrCapture(t *testing.T) {
915 t.Parallel()
916
917 cmd := helperCommand(t, "stderrfail")
918 _, err := cmd.Output()
919 ee, ok := err.(*exec.ExitError)
920 if !ok {
921 t.Fatalf("Output error type = %T; want ExitError", err)
922 }
923 got := string(ee.Stderr)
924 want := "some stderr text\n"
925 if got != want {
926 t.Errorf("ExitError.Stderr = %q; want %q", got, want)
927 }
928 }
929
930 func TestContext(t *testing.T) {
931 t.Parallel()
932
933 ctx, cancel := context.WithCancel(context.Background())
934 c := helperCommandContext(t, ctx, "pipetest")
935 stdin, err := c.StdinPipe()
936 if err != nil {
937 t.Fatal(err)
938 }
939 stdout, err := c.StdoutPipe()
940 if err != nil {
941 t.Fatal(err)
942 }
943 if err := c.Start(); err != nil {
944 t.Fatal(err)
945 }
946
947 if _, err := stdin.Write([]byte("O:hi\n")); err != nil {
948 t.Fatal(err)
949 }
950 buf := make([]byte, 5)
951 n, err := io.ReadFull(stdout, buf)
952 if n != len(buf) || err != nil || string(buf) != "O:hi\n" {
953 t.Fatalf("ReadFull = %d, %v, %q", n, err, buf[:n])
954 }
955 go cancel()
956
957 if err := c.Wait(); err == nil {
958 t.Fatal("expected Wait failure")
959 }
960 }
961
962 func TestContextCancel(t *testing.T) {
963 if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm64" {
964 maySkipHelperCommand("cat")
965 testenv.SkipFlaky(t, 42061)
966 }
967
968
969
970 t.Parallel()
971
972 ctx, cancel := context.WithCancel(context.Background())
973 defer cancel()
974 c := helperCommandContext(t, ctx, "cat")
975
976 stdin, err := c.StdinPipe()
977 if err != nil {
978 t.Fatal(err)
979 }
980 defer stdin.Close()
981
982 if err := c.Start(); err != nil {
983 t.Fatal(err)
984 }
985
986
987 if _, err := io.WriteString(stdin, "echo"); err != nil {
988 t.Fatal(err)
989 }
990
991 cancel()
992
993
994
995 start := time.Now()
996 delay := 1 * time.Millisecond
997 for {
998 if _, err := io.WriteString(stdin, "echo"); err != nil {
999 break
1000 }
1001
1002 if time.Since(start) > time.Minute {
1003
1004
1005 debug.SetTraceback("system")
1006 panic("canceling context did not stop program")
1007 }
1008
1009
1010
1011 delay *= 2
1012 if delay > 1*time.Second {
1013 delay = 1 * time.Second
1014 }
1015 time.Sleep(delay)
1016 }
1017
1018 if err := c.Wait(); err == nil {
1019 t.Error("program unexpectedly exited successfully")
1020 } else {
1021 t.Logf("exit status: %v", err)
1022 }
1023 }
1024
1025
1026 func TestDedupEnvEcho(t *testing.T) {
1027 t.Parallel()
1028
1029 cmd := helperCommand(t, "echoenv", "FOO")
1030 cmd.Env = append(cmd.Environ(), "FOO=bad", "FOO=good")
1031 out, err := cmd.CombinedOutput()
1032 if err != nil {
1033 t.Fatal(err)
1034 }
1035 if got, want := strings.TrimSpace(string(out)), "good"; got != want {
1036 t.Errorf("output = %q; want %q", got, want)
1037 }
1038 }
1039
1040 func TestEnvNULCharacter(t *testing.T) {
1041 if runtime.GOOS == "plan9" {
1042 t.Skip("plan9 explicitly allows NUL in the enviroment")
1043 }
1044 cmd := helperCommand(t, "echoenv", "FOO", "BAR")
1045 cmd.Env = append(cmd.Environ(), "FOO=foo\x00BAR=bar")
1046 out, err := cmd.CombinedOutput()
1047 if err == nil {
1048 t.Errorf("output = %q; want error", string(out))
1049 }
1050 }
1051
1052 func TestString(t *testing.T) {
1053 t.Parallel()
1054
1055 echoPath, err := exec.LookPath("echo")
1056 if err != nil {
1057 t.Skip(err)
1058 }
1059 tests := [...]struct {
1060 path string
1061 args []string
1062 want string
1063 }{
1064 {"echo", nil, echoPath},
1065 {"echo", []string{"a"}, echoPath + " a"},
1066 {"echo", []string{"a", "b"}, echoPath + " a b"},
1067 }
1068 for _, test := range tests {
1069 cmd := exec.Command(test.path, test.args...)
1070 if got := cmd.String(); got != test.want {
1071 t.Errorf("String(%q, %q) = %q, want %q", test.path, test.args, got, test.want)
1072 }
1073 }
1074 }
1075
1076 func TestStringPathNotResolved(t *testing.T) {
1077 t.Parallel()
1078
1079 _, err := exec.LookPath("makemeasandwich")
1080 if err == nil {
1081 t.Skip("wow, thanks")
1082 }
1083
1084 cmd := exec.Command("makemeasandwich", "-lettuce")
1085 want := "makemeasandwich -lettuce"
1086 if got := cmd.String(); got != want {
1087 t.Errorf("String(%q, %q) = %q, want %q", "makemeasandwich", "-lettuce", got, want)
1088 }
1089 }
1090
1091 func TestNoPath(t *testing.T) {
1092 err := new(exec.Cmd).Start()
1093 want := "exec: no command"
1094 if err == nil || err.Error() != want {
1095 t.Errorf("new(Cmd).Start() = %v, want %q", err, want)
1096 }
1097 }
1098
1099
1100
1101
1102 func TestDoubleStartLeavesPipesOpen(t *testing.T) {
1103 t.Parallel()
1104
1105 cmd := helperCommand(t, "pipetest")
1106 in, err := cmd.StdinPipe()
1107 if err != nil {
1108 t.Fatal(err)
1109 }
1110 out, err := cmd.StdoutPipe()
1111 if err != nil {
1112 t.Fatal(err)
1113 }
1114
1115 if err := cmd.Start(); err != nil {
1116 t.Fatal(err)
1117 }
1118 t.Cleanup(func() {
1119 if err := cmd.Wait(); err != nil {
1120 t.Error(err)
1121 }
1122 })
1123
1124 if err := cmd.Start(); err == nil || !strings.HasSuffix(err.Error(), "already started") {
1125 t.Fatalf("second call to Start returned a nil; want an 'already started' error")
1126 }
1127
1128 outc := make(chan []byte, 1)
1129 go func() {
1130 b, err := io.ReadAll(out)
1131 if err != nil {
1132 t.Error(err)
1133 }
1134 outc <- b
1135 }()
1136
1137 const msg = "O:Hello, pipe!\n"
1138
1139 _, err = io.WriteString(in, msg)
1140 if err != nil {
1141 t.Fatal(err)
1142 }
1143 in.Close()
1144
1145 b := <-outc
1146 if !bytes.Equal(b, []byte(msg)) {
1147 t.Fatalf("read %q from stdout pipe; want %q", b, msg)
1148 }
1149 }
1150
1151 func cmdHang(args ...string) {
1152 sleep, err := time.ParseDuration(args[0])
1153 if err != nil {
1154 panic(err)
1155 }
1156
1157 fs := flag.NewFlagSet("hang", flag.ExitOnError)
1158 exitOnInterrupt := fs.Bool("interrupt", false, "if true, commands should exit 0 on os.Interrupt")
1159 subsleep := fs.Duration("subsleep", 0, "amount of time for the 'hang' helper to leave an orphaned subprocess sleeping with stderr open")
1160 probe := fs.Duration("probe", 0, "if nonzero, the 'hang' helper should write to stderr at this interval, and exit nonzero if a write fails")
1161 read := fs.Bool("read", false, "if true, the 'hang' helper should read stdin to completion before sleeping")
1162 fs.Parse(args[1:])
1163
1164 pid := os.Getpid()
1165
1166 if *subsleep != 0 {
1167 cmd := exec.Command(exePath(nil), "hang", subsleep.String(), "-read=true", "-probe="+probe.String())
1168 cmd.Stdin = os.Stdin
1169 cmd.Stderr = os.Stderr
1170 out, err := cmd.StdoutPipe()
1171 if err != nil {
1172 fmt.Fprintln(os.Stderr, err)
1173 os.Exit(1)
1174 }
1175 cmd.Start()
1176
1177 buf := new(strings.Builder)
1178 if _, err := io.Copy(buf, out); err != nil {
1179 fmt.Fprintln(os.Stderr, err)
1180 cmd.Process.Kill()
1181 cmd.Wait()
1182 os.Exit(1)
1183 }
1184 fmt.Fprintf(os.Stderr, "%d: started %d: %v\n", pid, cmd.Process.Pid, cmd)
1185 go cmd.Wait()
1186 }
1187
1188 if *exitOnInterrupt {
1189 c := make(chan os.Signal, 1)
1190 signal.Notify(c, os.Interrupt)
1191 go func() {
1192 sig := <-c
1193 fmt.Fprintf(os.Stderr, "%d: received %v\n", pid, sig)
1194 os.Exit(0)
1195 }()
1196 } else {
1197 signal.Ignore(os.Interrupt)
1198 }
1199
1200
1201 os.Stdout.Close()
1202
1203 if *read {
1204 if pipeSignal != nil {
1205 signal.Ignore(pipeSignal)
1206 }
1207 r := bufio.NewReader(os.Stdin)
1208 for {
1209 line, err := r.ReadBytes('\n')
1210 if len(line) > 0 {
1211
1212 fmt.Fprintf(os.Stderr, "%d: read %s", pid, line)
1213 }
1214 if err != nil {
1215 fmt.Fprintf(os.Stderr, "%d: finished read: %v", pid, err)
1216 break
1217 }
1218 }
1219 }
1220
1221 if *probe != 0 {
1222 ticker := time.NewTicker(*probe)
1223 go func() {
1224 for range ticker.C {
1225 if _, err := fmt.Fprintf(os.Stderr, "%d: ok\n", pid); err != nil {
1226 os.Exit(1)
1227 }
1228 }
1229 }()
1230 }
1231
1232 if sleep != 0 {
1233 time.Sleep(sleep)
1234 fmt.Fprintf(os.Stderr, "%d: slept %v\n", pid, sleep)
1235 }
1236 }
1237
1238
1239
1240 type tickReader struct {
1241 interval time.Duration
1242 lastTick time.Time
1243 s string
1244 }
1245
1246 func newTickReader(interval time.Duration) *tickReader {
1247 return &tickReader{interval: interval}
1248 }
1249
1250 func (r *tickReader) Read(p []byte) (n int, err error) {
1251 if len(r.s) == 0 {
1252 if d := r.interval - time.Since(r.lastTick); d > 0 {
1253 time.Sleep(d)
1254 }
1255 r.lastTick = time.Now()
1256 r.s = r.lastTick.Format(time.RFC3339Nano + "\n")
1257 }
1258
1259 n = copy(p, r.s)
1260 r.s = r.s[n:]
1261 return n, nil
1262 }
1263
1264 func startHang(t *testing.T, ctx context.Context, hangTime time.Duration, interrupt os.Signal, waitDelay time.Duration, flags ...string) *exec.Cmd {
1265 t.Helper()
1266
1267 args := append([]string{hangTime.String()}, flags...)
1268 cmd := helperCommandContext(t, ctx, "hang", args...)
1269 cmd.Stdin = newTickReader(1 * time.Millisecond)
1270 cmd.Stderr = new(strings.Builder)
1271 if interrupt == nil {
1272 cmd.Cancel = nil
1273 } else {
1274 cmd.Cancel = func() error {
1275 return cmd.Process.Signal(interrupt)
1276 }
1277 }
1278 cmd.WaitDelay = waitDelay
1279 out, err := cmd.StdoutPipe()
1280 if err != nil {
1281 t.Fatal(err)
1282 }
1283
1284 t.Log(cmd)
1285 if err := cmd.Start(); err != nil {
1286 t.Fatal(err)
1287 }
1288
1289
1290 buf := new(strings.Builder)
1291 if _, err := io.Copy(buf, out); err != nil {
1292 t.Error(err)
1293 cmd.Process.Kill()
1294 cmd.Wait()
1295 t.FailNow()
1296 }
1297 if buf.Len() > 0 {
1298 t.Logf("stdout %v:\n%s", cmd.Args, buf)
1299 }
1300
1301 return cmd
1302 }
1303
1304 func TestWaitInterrupt(t *testing.T) {
1305 t.Parallel()
1306
1307
1308
1309
1310 const tooLong = 10 * time.Minute
1311
1312
1313
1314 t.Run("Wait", func(t *testing.T) {
1315 t.Parallel()
1316 cmd := startHang(t, context.Background(), 1*time.Millisecond, os.Kill, 0)
1317 err := cmd.Wait()
1318 t.Logf("stderr:\n%s", cmd.Stderr)
1319 t.Logf("[%d] %v", cmd.Process.Pid, err)
1320
1321 if err != nil {
1322 t.Errorf("Wait: %v; want <nil>", err)
1323 }
1324 if ps := cmd.ProcessState; !ps.Exited() {
1325 t.Errorf("cmd did not exit: %v", ps)
1326 } else if code := ps.ExitCode(); code != 0 {
1327 t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code)
1328 }
1329 })
1330
1331
1332
1333 t.Run("WaitDelay", func(t *testing.T) {
1334 if runtime.GOOS == "windows" {
1335 t.Skipf("skipping: os.Interrupt is not implemented on Windows")
1336 }
1337 t.Parallel()
1338
1339 ctx, cancel := context.WithCancel(context.Background())
1340 cmd := startHang(t, ctx, tooLong, nil, tooLong, "-interrupt=true")
1341 cancel()
1342
1343 time.Sleep(1 * time.Millisecond)
1344
1345
1346
1347 if err := cmd.Process.Signal(os.Interrupt); err != nil {
1348 t.Error(err)
1349 }
1350
1351 err := cmd.Wait()
1352 t.Logf("stderr:\n%s", cmd.Stderr)
1353 t.Logf("[%d] %v", cmd.Process.Pid, err)
1354
1355
1356
1357
1358
1359
1360 if err != nil {
1361 t.Errorf("Wait: %v; want nil", err)
1362 }
1363 if ps := cmd.ProcessState; !ps.Exited() {
1364 t.Errorf("cmd did not exit: %v", ps)
1365 } else if code := ps.ExitCode(); code != 0 {
1366 t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code)
1367 }
1368 })
1369
1370
1371
1372
1373
1374 t.Run("SIGKILL-hang", func(t *testing.T) {
1375 t.Parallel()
1376
1377 ctx, cancel := context.WithCancel(context.Background())
1378 cmd := startHang(t, ctx, tooLong, os.Kill, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms")
1379 cancel()
1380 err := cmd.Wait()
1381 t.Logf("stderr:\n%s", cmd.Stderr)
1382 t.Logf("[%d] %v", cmd.Process.Pid, err)
1383
1384
1385
1386
1387
1388
1389 if ee := new(*exec.ExitError); !errors.As(err, ee) {
1390 t.Errorf("Wait error = %v; want %T", err, *ee)
1391 }
1392 })
1393
1394
1395
1396
1397
1398
1399 t.Run("Exit-hang", func(t *testing.T) {
1400 t.Parallel()
1401
1402 cmd := startHang(t, context.Background(), 1*time.Millisecond, nil, 10*time.Millisecond, "-subsleep=10m", "-probe=1ms")
1403 err := cmd.Wait()
1404 t.Logf("stderr:\n%s", cmd.Stderr)
1405 t.Logf("[%d] %v", cmd.Process.Pid, err)
1406
1407
1408
1409
1410
1411 if !errors.Is(err, exec.ErrWaitDelay) {
1412 t.Errorf("Wait error = %v; want %T", err, exec.ErrWaitDelay)
1413 }
1414 })
1415
1416
1417
1418
1419 t.Run("SIGINT-ignored", func(t *testing.T) {
1420 if runtime.GOOS == "windows" {
1421 t.Skipf("skipping: os.Interrupt is not implemented on Windows")
1422 }
1423 t.Parallel()
1424
1425 ctx, cancel := context.WithCancel(context.Background())
1426 cmd := startHang(t, ctx, tooLong, os.Interrupt, 10*time.Millisecond, "-interrupt=false")
1427 cancel()
1428 err := cmd.Wait()
1429 t.Logf("stderr:\n%s", cmd.Stderr)
1430 t.Logf("[%d] %v", cmd.Process.Pid, err)
1431
1432
1433
1434 if ee := new(*exec.ExitError); !errors.As(err, ee) {
1435 t.Errorf("Wait error = %v; want %T", err, *ee)
1436 }
1437 })
1438
1439
1440
1441
1442
1443 t.Run("SIGINT-handled", func(t *testing.T) {
1444 if runtime.GOOS == "windows" {
1445 t.Skipf("skipping: os.Interrupt is not implemented on Windows")
1446 }
1447 t.Parallel()
1448
1449 ctx, cancel := context.WithCancel(context.Background())
1450 cmd := startHang(t, ctx, tooLong, os.Interrupt, 0, "-interrupt=true")
1451 cancel()
1452 err := cmd.Wait()
1453 t.Logf("stderr:\n%s", cmd.Stderr)
1454 t.Logf("[%d] %v", cmd.Process.Pid, err)
1455
1456 if !errors.Is(err, ctx.Err()) {
1457 t.Errorf("Wait error = %v; want %v", err, ctx.Err())
1458 }
1459 if ps := cmd.ProcessState; !ps.Exited() {
1460 t.Errorf("cmd did not exit: %v", ps)
1461 } else if code := ps.ExitCode(); code != 0 {
1462 t.Errorf("cmd.ProcessState.ExitCode() = %v; want 0", code)
1463 }
1464 })
1465
1466
1467
1468
1469 t.Run("SIGQUIT", func(t *testing.T) {
1470 if quitSignal == nil {
1471 t.Skipf("skipping: SIGQUIT is not supported on %v", runtime.GOOS)
1472 }
1473 t.Parallel()
1474
1475 ctx, cancel := context.WithCancel(context.Background())
1476 cmd := startHang(t, ctx, tooLong, quitSignal, 0)
1477 cancel()
1478 err := cmd.Wait()
1479 t.Logf("stderr:\n%s", cmd.Stderr)
1480 t.Logf("[%d] %v", cmd.Process.Pid, err)
1481
1482 if ee := new(*exec.ExitError); !errors.As(err, ee) {
1483 t.Errorf("Wait error = %v; want %v", err, ctx.Err())
1484 }
1485
1486 if ps := cmd.ProcessState; !ps.Exited() {
1487 t.Errorf("cmd did not exit: %v", ps)
1488 } else if code := ps.ExitCode(); code != 2 {
1489
1490 t.Errorf("cmd.ProcessState.ExitCode() = %v; want 2", code)
1491 }
1492
1493 if !strings.Contains(fmt.Sprint(cmd.Stderr), "\n\ngoroutine ") {
1494 t.Errorf("cmd.Stderr does not contain a goroutine dump")
1495 }
1496 })
1497 }
1498
1499 func TestCancelErrors(t *testing.T) {
1500 t.Parallel()
1501
1502
1503
1504 t.Run("success after error", func(t *testing.T) {
1505 t.Parallel()
1506
1507 ctx, cancel := context.WithCancel(context.Background())
1508 defer cancel()
1509
1510 cmd := helperCommandContext(t, ctx, "pipetest")
1511 stdin, err := cmd.StdinPipe()
1512 if err != nil {
1513 t.Fatal(err)
1514 }
1515
1516 errArbitrary := errors.New("arbitrary error")
1517 cmd.Cancel = func() error {
1518 stdin.Close()
1519 t.Logf("Cancel returning %v", errArbitrary)
1520 return errArbitrary
1521 }
1522 if err := cmd.Start(); err != nil {
1523 t.Fatal(err)
1524 }
1525 cancel()
1526
1527 err = cmd.Wait()
1528 t.Logf("[%d] %v", cmd.Process.Pid, err)
1529 if !errors.Is(err, errArbitrary) || err == errArbitrary {
1530 t.Errorf("Wait error = %v; want an error wrapping %v", err, errArbitrary)
1531 }
1532 })
1533
1534
1535
1536
1537
1538 t.Run("success after ErrProcessDone", func(t *testing.T) {
1539 t.Parallel()
1540
1541 ctx, cancel := context.WithCancel(context.Background())
1542 defer cancel()
1543
1544 cmd := helperCommandContext(t, ctx, "pipetest")
1545 stdin, err := cmd.StdinPipe()
1546 if err != nil {
1547 t.Fatal(err)
1548 }
1549
1550 stdout, err := cmd.StdoutPipe()
1551 if err != nil {
1552 t.Fatal(err)
1553 }
1554
1555
1556
1557
1558 interruptCalled := make(chan struct{})
1559 done := make(chan struct{})
1560 cmd.Cancel = func() error {
1561 close(interruptCalled)
1562 <-done
1563 t.Logf("Cancel returning an error wrapping ErrProcessDone")
1564 return fmt.Errorf("%w: stdout closed", os.ErrProcessDone)
1565 }
1566
1567 if err := cmd.Start(); err != nil {
1568 t.Fatal(err)
1569 }
1570
1571 cancel()
1572 <-interruptCalled
1573 stdin.Close()
1574 io.Copy(io.Discard, stdout)
1575 close(done)
1576
1577 err = cmd.Wait()
1578 t.Logf("[%d] %v", cmd.Process.Pid, err)
1579 if err != nil {
1580 t.Errorf("Wait error = %v; want nil", err)
1581 }
1582 })
1583
1584
1585
1586
1587 t.Run("killed after error", func(t *testing.T) {
1588 t.Parallel()
1589
1590 ctx, cancel := context.WithCancel(context.Background())
1591 defer cancel()
1592
1593 cmd := helperCommandContext(t, ctx, "pipetest")
1594 stdin, err := cmd.StdinPipe()
1595 if err != nil {
1596 t.Fatal(err)
1597 }
1598 defer stdin.Close()
1599
1600 errArbitrary := errors.New("arbitrary error")
1601 var interruptCalled atomic.Bool
1602 cmd.Cancel = func() error {
1603 t.Logf("Cancel called")
1604 interruptCalled.Store(true)
1605 return errArbitrary
1606 }
1607 cmd.WaitDelay = 1 * time.Millisecond
1608 if err := cmd.Start(); err != nil {
1609 t.Fatal(err)
1610 }
1611 cancel()
1612
1613 err = cmd.Wait()
1614 t.Logf("[%d] %v", cmd.Process.Pid, err)
1615
1616
1617
1618 if !interruptCalled.Load() {
1619 t.Errorf("Cancel was not called when the context was canceled")
1620 }
1621
1622
1623
1624
1625 if ee, ok := err.(*exec.ExitError); !ok {
1626 t.Errorf("Wait error = %v; want %T", err, *ee)
1627 }
1628 })
1629
1630
1631
1632
1633 t.Run("killed after spurious ErrProcessDone", func(t *testing.T) {
1634 t.Parallel()
1635
1636 ctx, cancel := context.WithCancel(context.Background())
1637 defer cancel()
1638
1639 cmd := helperCommandContext(t, ctx, "pipetest")
1640 stdin, err := cmd.StdinPipe()
1641 if err != nil {
1642 t.Fatal(err)
1643 }
1644 defer stdin.Close()
1645
1646 var interruptCalled atomic.Bool
1647 cmd.Cancel = func() error {
1648 t.Logf("Cancel returning an error wrapping ErrProcessDone")
1649 interruptCalled.Store(true)
1650 return fmt.Errorf("%w: stdout closed", os.ErrProcessDone)
1651 }
1652 cmd.WaitDelay = 1 * time.Millisecond
1653 if err := cmd.Start(); err != nil {
1654 t.Fatal(err)
1655 }
1656 cancel()
1657
1658 err = cmd.Wait()
1659 t.Logf("[%d] %v", cmd.Process.Pid, err)
1660
1661
1662
1663 if !interruptCalled.Load() {
1664 t.Errorf("Cancel was not called when the context was canceled")
1665 }
1666
1667
1668
1669
1670 if ee, ok := err.(*exec.ExitError); !ok {
1671 t.Errorf("Wait error of type %T; want %T", err, ee)
1672 }
1673 })
1674
1675
1676
1677
1678 t.Run("nonzero exit after error", func(t *testing.T) {
1679 t.Parallel()
1680
1681 ctx, cancel := context.WithCancel(context.Background())
1682 defer cancel()
1683
1684 cmd := helperCommandContext(t, ctx, "stderrfail")
1685 stderr, err := cmd.StderrPipe()
1686 if err != nil {
1687 t.Fatal(err)
1688 }
1689
1690 errArbitrary := errors.New("arbitrary error")
1691 interrupted := make(chan struct{})
1692 cmd.Cancel = func() error {
1693 close(interrupted)
1694 return errArbitrary
1695 }
1696 if err := cmd.Start(); err != nil {
1697 t.Fatal(err)
1698 }
1699 cancel()
1700 <-interrupted
1701 io.Copy(io.Discard, stderr)
1702
1703 err = cmd.Wait()
1704 t.Logf("[%d] %v", cmd.Process.Pid, err)
1705
1706 if ee, ok := err.(*exec.ExitError); !ok || ee.ProcessState.ExitCode() != 1 {
1707 t.Errorf("Wait error = %v; want exit status 1", err)
1708 }
1709 })
1710 }
1711
View as plain text