diff options
Diffstat (limited to 'src/os/exec')
-rw-r--r-- | src/os/exec/env_test.go | 16 | ||||
-rw-r--r-- | src/os/exec/example_test.go | 15 | ||||
-rw-r--r-- | src/os/exec/exec.go | 104 | ||||
-rw-r--r-- | src/os/exec/exec_linux_test.go | 2 | ||||
-rw-r--r-- | src/os/exec/exec_plan9.go | 20 | ||||
-rw-r--r-- | src/os/exec/exec_posix_test.go | 168 | ||||
-rw-r--r-- | src/os/exec/exec_test.go | 456 | ||||
-rw-r--r-- | src/os/exec/exec_unix.go | 20 | ||||
-rw-r--r-- | src/os/exec/exec_windows.go | 22 | ||||
-rw-r--r-- | src/os/exec/exec_windows_test.go | 42 | ||||
-rw-r--r-- | src/os/exec/lp_windows_test.go | 50 | ||||
-rw-r--r-- | src/os/exec/read3.go | 2 |
12 files changed, 638 insertions, 279 deletions
diff --git a/src/os/exec/env_test.go b/src/os/exec/env_test.go index b5ac398c274..112f1e654a1 100644 --- a/src/os/exec/env_test.go +++ b/src/os/exec/env_test.go @@ -18,17 +18,29 @@ func TestDedupEnv(t *testing.T) { { noCase: true, in: []string{"k1=v1", "k2=v2", "K1=v3"}, - want: []string{"K1=v3", "k2=v2"}, + want: []string{"k2=v2", "K1=v3"}, }, { noCase: false, in: []string{"k1=v1", "K1=V2", "k1=v3"}, - want: []string{"k1=v3", "K1=V2"}, + want: []string{"K1=V2", "k1=v3"}, }, { in: []string{"=a", "=b", "foo", "bar"}, want: []string{"=b", "foo", "bar"}, }, + { + // #49886: preserve weird Windows keys with leading "=" signs. + noCase: true, + in: []string{`=C:=C:\golang`, `=D:=D:\tmp`, `=D:=D:\`}, + want: []string{`=C:=C:\golang`, `=D:=D:\`}, + }, + { + // #52436: preserve invalid key-value entries (for now). + // (Maybe filter them out or error out on them at some point.) + in: []string{"dodgy", "entries"}, + want: []string{"dodgy", "entries"}, + }, } for _, tt := range tests { got := dedupEnvCase(tt.noCase, tt.in) diff --git a/src/os/exec/example_test.go b/src/os/exec/example_test.go index a66890be69f..bb166ceaf46 100644 --- a/src/os/exec/example_test.go +++ b/src/os/exec/example_test.go @@ -144,6 +144,21 @@ func ExampleCmd_CombinedOutput() { fmt.Printf("%s\n", stdoutStderr) } +func ExampleCmd_Environ() { + cmd := exec.Command("pwd") + + // Set Dir before calling cmd.Environ so that it will include an + // updated PWD variable (on platforms where that is used). + cmd.Dir = ".." + cmd.Env = append(cmd.Environ(), "POSIXLY_CORRECT=1") + + out, err := cmd.Output() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", out) +} + func ExampleCommandContext() { ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() diff --git a/src/os/exec/exec.go b/src/os/exec/exec.go index 845b737e28f..91c2e003d8d 100644 --- a/src/os/exec/exec.go +++ b/src/os/exec/exec.go @@ -223,13 +223,6 @@ func interfaceEqual(a, b any) bool { return a == b } -func (c *Cmd) envv() ([]string, error) { - if c.Env != nil { - return c.Env, nil - } - return execenv.Default(c.SysProcAttr) -} - func (c *Cmd) argv() []string { if len(c.Args) > 0 { return c.Args @@ -237,10 +230,6 @@ func (c *Cmd) argv() []string { return []string{c.Path} } -// skipStdinCopyError optionally specifies a function which reports -// whether the provided stdin copy error should be ignored. -var skipStdinCopyError func(error) bool - func (c *Cmd) stdin() (f *os.File, err error) { if c.Stdin == nil { f, err = os.Open(os.DevNull) @@ -264,7 +253,7 @@ func (c *Cmd) stdin() (f *os.File, err error) { c.closeAfterWait = append(c.closeAfterWait, pw) c.goroutine = append(c.goroutine, func() error { _, err := io.Copy(pw, c.Stdin) - if skip := skipStdinCopyError; skip != nil && skip(err) { + if skipStdinCopyError(err) { err = nil } if err1 := pw.Close(); err == nil { @@ -414,7 +403,7 @@ func (c *Cmd) Start() error { } c.childFiles = append(c.childFiles, c.ExtraFiles...) - envv, err := c.envv() + env, err := c.environ() if err != nil { return err } @@ -422,7 +411,7 @@ func (c *Cmd) Start() error { c.Process, err = os.StartProcess(c.Path, c.argv(), &os.ProcAttr{ Dir: c.Dir, Files: c.childFiles, - Env: addCriticalEnv(dedupEnv(envv)), + Env: env, Sys: c.SysProcAttr, }) if err != nil { @@ -735,6 +724,54 @@ func minInt(a, b int) int { return b } +// environ returns a best-effort copy of the environment in which the command +// would be run as it is currently configured. If an error occurs in computing +// the environment, it is returned alongside the best-effort copy. +func (c *Cmd) environ() ([]string, error) { + var err error + + env := c.Env + if env == nil { + env, err = execenv.Default(c.SysProcAttr) + if err != nil { + env = os.Environ() + // Note that the non-nil err is preserved despite env being overridden. + } + + if c.Dir != "" { + switch runtime.GOOS { + case "windows", "plan9": + // Windows and Plan 9 do not use the PWD variable, so we don't need to + // keep it accurate. + default: + // On POSIX platforms, PWD represents “an absolute pathname of the + // current working directory.” Since we are changing the working + // directory for the command, we should also update PWD to reflect that. + // + // Unfortunately, we didn't always do that, so (as proposed in + // https://go.dev/issue/50599) to avoid unintended collateral damage we + // only implicitly update PWD when Env is nil. That way, we're much + // less likely to override an intentional change to the variable. + if pwd, absErr := filepath.Abs(c.Dir); absErr == nil { + env = append(env, "PWD="+pwd) + } else if err == nil { + err = absErr + } + } + } + } + + return addCriticalEnv(dedupEnv(env)), err +} + +// Environ returns a copy of the environment in which the command would be run +// as it is currently configured. +func (c *Cmd) Environ() []string { + // Intentionally ignore errors: environ returns a best-effort environment no matter what. + env, _ := c.environ() + return env +} + // dedupEnv returns a copy of env with any duplicates removed, in favor of // later values. // Items not of the normal environment "key=value" form are preserved unchanged. @@ -745,24 +782,47 @@ func dedupEnv(env []string) []string { // dedupEnvCase is dedupEnv with a case option for testing. // If caseInsensitive is true, the case of keys is ignored. func dedupEnvCase(caseInsensitive bool, env []string) []string { + // Construct the output in reverse order, to preserve the + // last occurrence of each key. out := make([]string, 0, len(env)) - saw := make(map[string]int, len(env)) // key => index into out - for _, kv := range env { - k, _, ok := strings.Cut(kv, "=") - if !ok { - out = append(out, kv) + saw := make(map[string]bool, len(env)) + for n := len(env); n > 0; n-- { + kv := env[n-1] + + i := strings.Index(kv, "=") + if i == 0 { + // We observe in practice keys with a single leading "=" on Windows. + // TODO(#49886): Should we consume only the first leading "=" as part + // of the key, or parse through arbitrarily many of them until a non-"="? + i = strings.Index(kv[1:], "=") + 1 + } + if i < 0 { + if kv != "" { + // The entry is not of the form "key=value" (as it is required to be). + // Leave it as-is for now. + // TODO(#52436): should we strip or reject these bogus entries? + out = append(out, kv) + } continue } + k := kv[:i] if caseInsensitive { k = strings.ToLower(k) } - if dupIdx, isDup := saw[k]; isDup { - out[dupIdx] = kv + if saw[k] { continue } - saw[k] = len(out) + + saw[k] = true out = append(out, kv) } + + // Now reverse the slice to restore the original order. + for i := 0; i < len(out)/2; i++ { + j := len(out) - i - 1 + out[i], out[j] = out[j], out[i] + } + return out } diff --git a/src/os/exec/exec_linux_test.go b/src/os/exec/exec_linux_test.go index 4a37c96e63a..b9f6b7b7672 100644 --- a/src/os/exec/exec_linux_test.go +++ b/src/os/exec/exec_linux_test.go @@ -22,7 +22,7 @@ import ( ) func init() { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + if os.Getenv("GO_EXEC_TEST_PID") == "" { return } diff --git a/src/os/exec/exec_plan9.go b/src/os/exec/exec_plan9.go index 21ac7b765f1..8920bec1f5e 100644 --- a/src/os/exec/exec_plan9.go +++ b/src/os/exec/exec_plan9.go @@ -6,14 +6,14 @@ package exec import "io/fs" -func init() { - skipStdinCopyError = func(err error) bool { - // Ignore hungup errors copying to stdin if the program - // completed successfully otherwise. - // See Issue 35753. - pe, ok := err.(*fs.PathError) - return ok && - pe.Op == "write" && pe.Path == "|1" && - pe.Err.Error() == "i/o on hungup channel" - } +// skipStdinCopyError optionally specifies a function which reports +// whether the provided stdin copy error should be ignored. +func skipStdinCopyError(err error) bool { + // Ignore hungup errors copying to stdin if the program + // completed successfully otherwise. + // See Issue 35753. + pe, ok := err.(*fs.PathError) + return ok && + pe.Op == "write" && pe.Path == "|1" && + pe.Err.Error() == "i/o on hungup channel" } diff --git a/src/os/exec/exec_posix_test.go b/src/os/exec/exec_posix_test.go index ce83a9e4b3a..f0401377e8e 100644 --- a/src/os/exec/exec_posix_test.go +++ b/src/os/exec/exec_posix_test.go @@ -7,16 +7,46 @@ package exec_test import ( + "fmt" + "internal/testenv" + "os" "os/user" + "path/filepath" + "reflect" "runtime" "strconv" + "strings" "syscall" "testing" "time" ) +func init() { + registerHelperCommand("pwd", cmdPwd) + registerHelperCommand("sleep", cmdSleep) +} + +func cmdPwd(...string) { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(pwd) +} + +func cmdSleep(args ...string) { + n, err := strconv.Atoi(args[0]) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + time.Sleep(time.Duration(n) * time.Second) +} + func TestCredentialNoSetGroups(t *testing.T) { if runtime.GOOS == "android" { + maySkipHelperCommand("echo") t.Skip("unsupported on Android") } @@ -55,7 +85,7 @@ func TestCredentialNoSetGroups(t *testing.T) { func TestWaitid(t *testing.T) { t.Parallel() - cmd := helperCommand(t, "sleep") + cmd := helperCommand(t, "sleep", "3") if err := cmd.Start(); err != nil { t.Fatal(err) } @@ -86,3 +116,139 @@ func TestWaitid(t *testing.T) { <-ch } + +// https://go.dev/issue/50599: if Env is not set explicitly, setting Dir should +// implicitly update PWD to the correct path, and Environ should list the +// updated value. +func TestImplicitPWD(t *testing.T) { + t.Parallel() + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + cases := []struct { + name string + dir string + want string + }{ + {"empty", "", cwd}, + {"dot", ".", cwd}, + {"dotdot", "..", filepath.Dir(cwd)}, + {"PWD", cwd, cwd}, + {"PWDdotdot", cwd + string(filepath.Separator) + "..", filepath.Dir(cwd)}, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + if cmd.Env != nil { + t.Fatalf("test requires helperCommand not to set Env field") + } + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.want} + if tc.dir == "" { + if _, ok := os.LookupEnv("PWD"); !ok { + wantPWDs = nil + } + } + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\n`pwd` reported\n\t%s", tc.dir, got) + if got != tc.want { + t.Errorf("want\n\t%s", tc.want) + } + }) + } +} + +// However, if cmd.Env is set explicitly, setting Dir should not override it. +// (This checks that the implementation for https://go.dev/issue/50599 doesn't +// break existing users who may have explicitly mismatched the PWD variable.) +func TestExplicitPWD(t *testing.T) { + maySkipHelperCommand("pwd") + testenv.MustHaveSymlink(t) + + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + link := filepath.Join(t.TempDir(), "link") + if err := os.Symlink(cwd, link); err != nil { + t.Fatal(err) + } + + // Now link is another equally-valid name for cwd. If we set Dir to one and + // PWD to the other, the subprocess should report the PWD version. + cases := []struct { + name string + dir string + pwd string + }{ + {name: "original PWD", pwd: cwd}, + {name: "link PWD", pwd: link}, + {name: "in link with original PWD", dir: link, pwd: cwd}, + {name: "in dir with link PWD", dir: cwd, pwd: link}, + // Ideally we would also like to test what happens if we set PWD to + // something totally bogus (or the empty string), but then we would have no + // idea what output the subprocess should actually produce: cwd itself may + // contain symlinks preserved from the PWD value in the test's environment. + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cmd := helperCommand(t, "pwd") + // This is intentionally opposite to the usual order of setting cmd.Dir + // and then calling cmd.Environ. Here, we *want* PWD not to match cmd.Dir, + // so we don't care whether cmd.Dir is reflected in cmd.Environ. + cmd.Env = append(cmd.Environ(), "PWD="+tc.pwd) + cmd.Dir = tc.dir + + var pwds []string + for _, kv := range cmd.Environ() { + if strings.HasPrefix(kv, "PWD=") { + pwds = append(pwds, strings.TrimPrefix(kv, "PWD=")) + } + } + + wantPWDs := []string{tc.pwd} + if !reflect.DeepEqual(pwds, wantPWDs) { + t.Errorf("PWD entries in cmd.Environ():\n\t%s\nwant:\n\t%s", strings.Join(pwds, "\n\t"), strings.Join(wantPWDs, "\n\t")) + } + + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%v:\n%s", err, cmd.Stderr) + } + got := strings.Trim(string(out), "\r\n") + t.Logf("in\n\t%s\nwith PWD=%s\nsubprocess os.Getwd() reported\n\t%s", tc.dir, tc.pwd, got) + if got != tc.pwd { + t.Errorf("want\n\t%s", tc.pwd) + } + }) + } +} diff --git a/src/os/exec/exec_test.go b/src/os/exec/exec_test.go index 73aa35f1aed..c593cbd11d1 100644 --- a/src/os/exec/exec_test.go +++ b/src/os/exec/exec_test.go @@ -11,6 +11,7 @@ import ( "bufio" "bytes" "context" + "flag" "fmt" "internal/poll" "internal/testenv" @@ -27,6 +28,7 @@ import ( "runtime" "strconv" "strings" + "sync" "testing" "time" ) @@ -36,7 +38,7 @@ import ( var haveUnexpectedFDs bool func init() { - if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" { + if os.Getenv("GO_EXEC_TEST_PID") != "" { return } if runtime.GOOS == "windows" { @@ -54,22 +56,253 @@ func init() { } } -func helperCommandContext(t *testing.T, ctx context.Context, s ...string) (cmd *exec.Cmd) { +// TestMain allows the test binary to impersonate many other binaries, +// some of which may manipulate os.Stdin, os.Stdout, and/or os.Stderr +// (and thus cannot run as an ordinary Test function, since the testing +// package monkey-patches those variables before running tests). +func TestMain(m *testing.M) { + flag.Parse() + + pid := os.Getpid() + if os.Getenv("GO_EXEC_TEST_PID") == "" { + os.Setenv("GO_EXEC_TEST_PID", strconv.Itoa(pid)) + + code := m.Run() + if code == 0 && flag.Lookup("test.run").Value.String() == "" && flag.Lookup("test.list").Value.String() == "" { + for cmd := range helperCommands { + if _, ok := helperCommandUsed.Load(cmd); !ok { + fmt.Fprintf(os.Stderr, "helper command unused: %q\n", cmd) + code = 1 + } + } + } + os.Exit(code) + } + + args := flag.Args() + if len(args) == 0 { + fmt.Fprintf(os.Stderr, "No command\n") + os.Exit(2) + } + + cmd, args := args[0], args[1:] + f, ok := helperCommands[cmd] + if !ok { + fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) + os.Exit(2) + } + f(args...) + os.Exit(0) +} + +// registerHelperCommand registers a command that the test process can impersonate. +// A command should be registered in the same source file in which it is used. +// If all tests are run and pass, all registered commands must be used. +// (This prevents stale commands from accreting if tests are removed or +// refactored over time.) +func registerHelperCommand(name string, f func(...string)) { + if helperCommands[name] != nil { + panic("duplicate command registered: " + name) + } + helperCommands[name] = f +} + +// maySkipHelperCommand records that the test that uses the named helper command +// was invoked, but may call Skip on the test before actually calling +// helperCommand. +func maySkipHelperCommand(name string) { + helperCommandUsed.Store(name, true) +} + +// helperCommand returns an exec.Cmd that will run the named helper command. +func helperCommand(t *testing.T, name string, args ...string) *exec.Cmd { + t.Helper() + return helperCommandContext(t, nil, name, args...) +} + +// helperCommandContext is like helperCommand, but also accepts a Context under +// which to run the command. +func helperCommandContext(t *testing.T, ctx context.Context, name string, args ...string) (cmd *exec.Cmd) { + helperCommandUsed.LoadOrStore(name, true) + + t.Helper() testenv.MustHaveExec(t) - cs := []string{"-test.run=TestHelperProcess", "--"} - cs = append(cs, s...) + cs := append([]string{name}, args...) if ctx != nil { - cmd = exec.CommandContext(ctx, os.Args[0], cs...) + cmd = exec.CommandContext(ctx, exePath(t), cs...) } else { - cmd = exec.Command(os.Args[0], cs...) + cmd = exec.Command(exePath(t), cs...) } - cmd.Env = append(os.Environ(), "GO_WANT_HELPER_PROCESS=1") return cmd } -func helperCommand(t *testing.T, s ...string) *exec.Cmd { - return helperCommandContext(t, nil, s...) +// exePath returns the path to the running executable. +func exePath(t testing.TB) string { + exeOnce.Do(func() { + // Use os.Executable instead of os.Args[0] in case the caller modifies + // cmd.Dir: if the test binary is invoked like "./exec.test", it should + // not fail spuriously. + exeOnce.path, exeOnce.err = os.Executable() + }) + + if exeOnce.err != nil { + if t == nil { + panic(exeOnce.err) + } + t.Fatal(exeOnce.err) + } + + return exeOnce.path +} + +var exeOnce struct { + path string + err error + sync.Once +} + +var helperCommandUsed sync.Map + +var helperCommands = map[string]func(...string){ + "echo": cmdEcho, + "echoenv": cmdEchoEnv, + "cat": cmdCat, + "pipetest": cmdPipeTest, + "stdinClose": cmdStdinClose, + "exit": cmdExit, + "describefiles": cmdDescribeFiles, + "extraFilesAndPipes": cmdExtraFilesAndPipes, + "stderrfail": cmdStderrFail, + "yes": cmdYes, +} + +func cmdEcho(args ...string) { + iargs := []any{} + for _, s := range args { + iargs = append(iargs, s) + } + fmt.Println(iargs...) +} + +func cmdEchoEnv(args ...string) { + for _, s := range args { + fmt.Println(os.Getenv(s)) + } +} + +func cmdCat(args ...string) { + if len(args) == 0 { + io.Copy(os.Stdout, os.Stdin) + return + } + exit := 0 + for _, fn := range args { + f, err := os.Open(fn) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + exit = 2 + } else { + defer f.Close() + io.Copy(os.Stdout, f) + } + } + os.Exit(exit) +} + +func cmdPipeTest(...string) { + bufr := bufio.NewReader(os.Stdin) + for { + line, _, err := bufr.ReadLine() + if err == io.EOF { + break + } else if err != nil { + os.Exit(1) + } + if bytes.HasPrefix(line, []byte("O:")) { + os.Stdout.Write(line) + os.Stdout.Write([]byte{'\n'}) + } else if bytes.HasPrefix(line, []byte("E:")) { + os.Stderr.Write(line) + os.Stderr.Write([]byte{'\n'}) + } else { + os.Exit(1) + } + } +} + +func cmdStdinClose(...string) { + b, err := io.ReadAll(os.Stdin) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + if s := string(b); s != stdinCloseTestString { + fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString) + os.Exit(1) + } +} + +func cmdExit(args ...string) { + n, _ := strconv.Atoi(args[0]) + os.Exit(n) +} + +func cmdDescribeFiles(args ...string) { + f := os.NewFile(3, fmt.Sprintf("fd3")) + ln, err := net.FileListener(f) + if err == nil { + fmt.Printf("fd3: listener %s\n", ln.Addr()) + ln.Close() + } +} + +func cmdExtraFilesAndPipes(args ...string) { + n, _ := strconv.Atoi(args[0]) + pipes := make([]*os.File, n) + for i := 0; i < n; i++ { + pipes[i] = os.NewFile(uintptr(3+i), strconv.Itoa(i)) + } + response := "" + for i, r := range pipes { + ch := make(chan string, 1) + go func(c chan string) { + buf := make([]byte, 10) + n, err := r.Read(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "Child: read error: %v on pipe %d\n", err, i) + os.Exit(1) + } + c <- string(buf[:n]) + close(c) + }(ch) + select { + case m := <-ch: + response = response + m + case <-time.After(5 * time.Second): + fmt.Fprintf(os.Stderr, "Child: Timeout reading from pipe: %d\n", i) + os.Exit(1) + } + } + fmt.Fprintf(os.Stderr, "child: %s", response) +} + +func cmdStderrFail(...string) { + fmt.Fprintf(os.Stderr, "some stderr text\n") + os.Exit(1) +} + +func cmdYes(args ...string) { + if len(args) == 0 { + args = []string{"y"} + } + s := strings.Join(args, " ") + "\n" + for { + _, err := os.Stdout.WriteString(s) + if err != nil { + os.Exit(1) + } + } } func TestEcho(t *testing.T) { @@ -83,7 +316,7 @@ func TestEcho(t *testing.T) { } func TestCommandRelativeName(t *testing.T) { - testenv.MustHaveExec(t) + cmd := helperCommand(t, "echo", "foo") // Run our own binary as a relative path // (e.g. "_test/exec.test") our parent directory. @@ -98,9 +331,8 @@ func TestCommandRelativeName(t *testing.T) { t.Skipf("skipping; unexpected shallow dir of %q", dir) } - cmd := exec.Command(filepath.Join(dirBase, base), "-test.run=TestHelperProcess", "--", "echo", "foo") + cmd.Path = filepath.Join(dirBase, base) cmd.Dir = parentDir - cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} out, err := cmd.Output() if err != nil { @@ -159,7 +391,7 @@ func TestCatGoodAndBadFile(t *testing.T) { if !strings.HasPrefix(errLine, "Error: open /bogus/file.foo") { t.Errorf("expected stderr to complain about file; got %q", errLine) } - if !strings.Contains(body, "func TestHelperProcess(t *testing.T)") { + if !strings.Contains(body, "func TestCatGoodAndBadFile(t *testing.T)") { t.Errorf("expected test code; got %q (len %d)", body, len(body)) } } @@ -394,6 +626,7 @@ func TestPipeLookPathLeak(t *testing.T) { } func TestExtraFilesFDShuffle(t *testing.T) { + maySkipHelperCommand("extraFilesAndPipes") testenv.SkipFlaky(t, 5780) switch runtime.GOOS { case "windows": @@ -619,6 +852,7 @@ func TestExtraFiles(t *testing.T) { func TestExtraFilesRace(t *testing.T) { if runtime.GOOS == "windows" { + maySkipHelperCommand("describefiles") t.Skip("no operating system support; skipping") } listen := func() net.Listener { @@ -676,167 +910,6 @@ func TestExtraFilesRace(t *testing.T) { } } -// TestHelperProcess isn't a real test. It's used as a helper process -// for TestParameterRun. -func TestHelperProcess(*testing.T) { - if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { - return - } - defer os.Exit(0) - - args := os.Args - for len(args) > 0 { - if args[0] == "--" { - args = args[1:] - break - } - args = args[1:] - } - if len(args) == 0 { - fmt.Fprintf(os.Stderr, "No command\n") - os.Exit(2) - } - - cmd, args := args[0], args[1:] - switch cmd { - case "echo": - iargs := []any{} - for _, s := range args { - iargs = append(iargs, s) - } - fmt.Println(iargs...) - case "echoenv": - for _, s := range args { - fmt.Println(os.Getenv(s)) - } - os.Exit(0) - case "cat": - if len(args) == 0 { - io.Copy(os.Stdout, os.Stdin) - return - } - exit := 0 - for _, fn := range args { - f, err := os.Open(fn) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - exit = 2 - } else { - defer f.Close() - io.Copy(os.Stdout, f) - } - } - os.Exit(exit) - case "pipetest": - bufr := bufio.NewReader(os.Stdin) - for { - line, _, err := bufr.ReadLine() - if err == io.EOF { - break - } else if err != nil { - os.Exit(1) - } - if bytes.HasPrefix(line, []byte("O:")) { - os.Stdout.Write(line) - os.Stdout.Write([]byte{'\n'}) - } else if bytes.HasPrefix(line, []byte("E:")) { - os.Stderr.Write(line) - os.Stderr.Write([]byte{'\n'}) - } else { - os.Exit(1) - } - } - case "stdinClose": - b, err := io.ReadAll(os.Stdin) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - if s := string(b); s != stdinCloseTestString { - fmt.Fprintf(os.Stderr, "Error: Read %q, want %q", s, stdinCloseTestString) - os.Exit(1) - } - os.Exit(0) - case "exit": - n, _ := strconv.Atoi(args[0]) - os.Exit(n) - case "describefiles": - f := os.NewFile(3, fmt.Sprintf("fd3")) - ln, err := net.FileListener(f) - if err == nil { - fmt.Printf("fd3: listener %s\n", ln.Addr()) - ln.Close() - } - os.Exit(0) - case "extraFilesAndPipes": - n, _ := strconv.Atoi(args[0]) - pipes := make([]*os.File, n) - for i := 0; i < n; i++ { - pipes[i] = os.NewFile(uintptr(3+i), strconv.Itoa(i)) - } - response := "" - for i, r := range pipes { - ch := make(chan string, 1) - go func(c chan string) { - buf := make([]byte, 10) - n, err := r.Read(buf) - if err != nil { - fmt.Fprintf(os.Stderr, "Child: read error: %v on pipe %d\n", err, i) - os.Exit(1) - } - c <- string(buf[:n]) - close(c) - }(ch) - select { - case m := <-ch: - response = response + m - case <-time.After(5 * time.Second): - fmt.Fprintf(os.Stderr, "Child: Timeout reading from pipe: %d\n", i) - os.Exit(1) - } - } - fmt.Fprintf(os.Stderr, "child: %s", response) - os.Exit(0) - case "exec": - cmd := exec.Command(args[1]) - cmd.Dir = args[0] - output, err := cmd.CombinedOutput() - if err != nil { - fmt.Fprintf(os.Stderr, "Child: %s %s", err, string(output)) - os.Exit(1) - } - fmt.Printf("%s", string(output)) - os.Exit(0) - case "lookpath": - p, err := exec.LookPath(args[0]) - if err != nil { - fmt.Fprintf(os.Stderr, "LookPath failed: %v\n", err) - os.Exit(1) - } - fmt.Print(p) - os.Exit(0) - case "stderrfail": - fmt.Fprintf(os.Stderr, "some stderr text\n") - os.Exit(1) - case "sleep": - time.Sleep(3 * time.Second) - os.Exit(0) - case "pipehandle": - handle, _ := strconv.ParseUint(args[0], 16, 64) - pipe := os.NewFile(uintptr(handle), "") - _, err := fmt.Fprint(pipe, args[1]) - if err != nil { - fmt.Fprintf(os.Stderr, "writing to pipe failed: %v\n", err) - os.Exit(1) - } - pipe.Close() - os.Exit(0) - default: - fmt.Fprintf(os.Stderr, "Unknown command %q\n", cmd) - os.Exit(2) - } -} - type delayedInfiniteReader struct{} func (delayedInfiniteReader) Read(b []byte) (int, error) { @@ -849,8 +922,6 @@ func (delayedInfiniteReader) Read(b []byte) (int, error) { // Issue 9173: ignore stdin pipe writes if the program completes successfully. func TestIgnorePipeErrorOnSuccess(t *testing.T) { - testenv.MustHaveExec(t) - testWith := func(r io.Reader) func(*testing.T) { return func(t *testing.T) { cmd := helperCommand(t, "echo", "foo") @@ -876,12 +947,7 @@ func (w *badWriter) Write(data []byte) (int, error) { } func TestClosePipeOnCopyError(t *testing.T) { - testenv.MustHaveExec(t) - - if runtime.GOOS == "windows" || runtime.GOOS == "plan9" { - t.Skipf("skipping test on %s - no yes command", runtime.GOOS) - } - cmd := exec.Command("yes") + cmd := helperCommand(t, "yes") cmd.Stdout = new(badWriter) c := make(chan int, 1) go func() { @@ -900,8 +966,6 @@ func TestClosePipeOnCopyError(t *testing.T) { } func TestOutputStderrCapture(t *testing.T) { - testenv.MustHaveExec(t) - cmd := helperCommand(t, "stderrfail") _, err := cmd.Output() ee, ok := err.(*exec.ExitError) @@ -955,6 +1019,7 @@ func TestContext(t *testing.T) { func TestContextCancel(t *testing.T) { if runtime.GOOS == "netbsd" && runtime.GOARCH == "arm64" { + maySkipHelperCommand("cat") testenv.SkipFlaky(t, 42061) } @@ -1016,10 +1081,8 @@ func TestContextCancel(t *testing.T) { // test that environment variables are de-duped. func TestDedupEnvEcho(t *testing.T) { - testenv.MustHaveExec(t) - cmd := helperCommand(t, "echoenv", "FOO") - cmd.Env = append(cmd.Env, "FOO=bad", "FOO=good") + cmd.Env = append(cmd.Environ(), "FOO=bad", "FOO=good") |