aboutsummaryrefslogtreecommitdiff
path: root/src/os/exec
diff options
context:
space:
mode:
Diffstat (limited to 'src/os/exec')
-rw-r--r--src/os/exec/env_test.go16
-rw-r--r--src/os/exec/example_test.go15
-rw-r--r--src/os/exec/exec.go104
-rw-r--r--src/os/exec/exec_linux_test.go2
-rw-r--r--src/os/exec/exec_plan9.go20
-rw-r--r--src/os/exec/exec_posix_test.go168
-rw-r--r--src/os/exec/exec_test.go456
-rw-r--r--src/os/exec/exec_unix.go20
-rw-r--r--src/os/exec/exec_windows.go22
-rw-r--r--src/os/exec/exec_windows_test.go42
-rw-r--r--src/os/exec/lp_windows_test.go50
-rw-r--r--src/os/exec/read3.go2
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")