"time"
)
-// HasExec reports whether the current system can start new processes
+// MustHaveExec checks that the current system can start new processes
// using os.StartProcess or (more commonly) exec.Command.
-func HasExec() bool {
+// If not, MustHaveExec calls t.Skip with an explanation.
+//
+// On some platforms MustHaveExec checks for exec support by re-executing the
+// current executable, which must be a binary built by 'go test'.
+// We intentionally do not provide a HasExec function because of the risk of
+// inappropriate recursion in TestMain functions.
+//
+// To check for exec support outside of a test, just try to exec the command.
+// If exec is not supported, testenv.SyscallIsNotSupported will return true
+// for the resulting error.
+func MustHaveExec(t testing.TB) {
tryExecOnce.Do(func() {
tryExecOk = tryExec()
})
- return tryExecOk
+ if !tryExecOk {
+ t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
+ }
}
var (
return
}
- // We know that this is a test executable.
- // We should be able to run it with a no-op flag and the original test
- // execution environment to check for overall exec support.
-
- // Save the original environment during init for use in the check. A test
- // binary may modify its environment before calling HasExec to change its
- // behavior// (such as mimicking a command-line tool), and that modified
- // environment might cause our self-test to behave unpredictably.
- origEnv := os.Environ()
-
+ // We know that this is a test executable. We should be able to run it with a
+ // no-op flag to check for overall exec support.
tryExec = func() bool {
exe, err := os.Executable()
if err != nil {
}
}
-// MustHaveExec checks that the current system can start new processes
-// using os.StartProcess or (more commonly) exec.Command.
-// If not, MustHaveExec calls t.Skip with an explanation.
-func MustHaveExec(t testing.TB) {
- if !HasExec() {
- t.Skipf("skipping test: cannot exec subprocess on %s/%s", runtime.GOOS, runtime.GOARCH)
- }
-}
-
var execPaths sync.Map // path -> error
// MustHaveExecPath checks that the current system can start the named executable
"testing"
)
+// Save the original environment during init for use in checks. A test
+// binary may modify its environment before calling HasExec to change its
+// behavior (such as mimicking a command-line tool), and that modified
+// environment might cause environment checks to behave erratically.
+var origEnv = os.Environ()
+
// Builder reports the name of the builder running this test
// (for example, "linux-amd64" or "windows-386-gce").
// If the test is not running on the build infrastructure,
return false
}
- if !HasExec() {
- // If we can't exec anything at all, we certainly can't exec 'go build'.
- return false
- }
-
- if platform.MustLinkExternal(runtime.GOOS, runtime.GOARCH, false) {
- // We can assume that we always have a complete Go toolchain available.
- // However, this platform requires a C linker to build even pure Go
- // programs, including tests. Do we have one in the test environment?
- // (On Android, for example, the device running the test might not have a
- // C toolchain installed.)
- //
- // If CC is set explicitly, assume that we do. Otherwise, use 'go env CC'
- // to determine which toolchain it would use by default.
- if os.Getenv("CC") == "" {
- if _, err := findCC(); err != nil {
- return false
- }
- }
- }
-
- return true
-}
-
-func findCC() (string, error) {
- ccOnce.Do(func() {
- goTool, err := findGoTool()
+ goBuildOnce.Do(func() {
+ // To run 'go build', we need to be able to exec a 'go' command.
+ // We somewhat arbitrarily choose to exec 'go tool -n compile' because that
+ // also confirms that cmd/go can find the compiler. (Before CL 472096,
+ // we sometimes ended up with cmd/go installed in the test environment
+ // without a cmd/compile it could use to actually build things.)
+ cmd := exec.Command("go", "tool", "-n", "compile")
+ cmd.Env = origEnv
+ out, err := cmd.Output()
if err != nil {
- ccErr = err
+ goBuildErr = fmt.Errorf("%v: %w", cmd, err)
return
}
-
- cmd := exec.Command(goTool, "env", "CC")
- out, err := cmd.Output()
out = bytes.TrimSpace(out)
- if err != nil {
- ccErr = fmt.Errorf("%v: %w", cmd, err)
+ if len(out) == 0 {
+ goBuildErr = fmt.Errorf("%v: no tool reported", cmd)
return
- } else if len(out) == 0 {
- ccErr = fmt.Errorf("%v: no CC reported", cmd)
+ }
+ if _, err := exec.LookPath(string(out)); err != nil {
+ goBuildErr = err
return
}
- cc := string(out)
- ccPath, ccErr = exec.LookPath(cc)
+ if platform.MustLinkExternal(runtime.GOOS, runtime.GOARCH, false) {
+ // We can assume that we always have a complete Go toolchain available.
+ // However, this platform requires a C linker to build even pure Go
+ // programs, including tests. Do we have one in the test environment?
+ // (On Android, for example, the device running the test might not have a
+ // C toolchain installed.)
+ //
+ // If CC is set explicitly, assume that we do. Otherwise, use 'go env CC'
+ // to determine which toolchain it would use by default.
+ if os.Getenv("CC") == "" {
+ cmd := exec.Command("go", "env", "CC")
+ cmd.Env = origEnv
+ out, err := cmd.Output()
+ if err != nil {
+ goBuildErr = fmt.Errorf("%v: %w", cmd, err)
+ return
+ }
+ out = bytes.TrimSpace(out)
+ if len(out) == 0 {
+ goBuildErr = fmt.Errorf("%v: no CC reported", cmd)
+ return
+ }
+ _, goBuildErr = exec.LookPath(string(out))
+ }
+ }
})
- return ccPath, ccErr
+
+ return goBuildErr == nil
}
var (
- ccOnce sync.Once
- ccPath string
- ccErr error
+ goBuildOnce sync.Once
+ goBuildErr error
)
// MustHaveGoBuild checks that the current system can build programs with “go build”
// If not, MustHaveGoBuild calls t.Skip with an explanation.
func MustHaveGoBuild(t testing.TB) {
if os.Getenv("GO_GCFLAGS") != "" {
+ t.Helper()
t.Skipf("skipping test: 'go build' not compatible with setting $GO_GCFLAGS")
}
if !HasGoBuild() {
- t.Skipf("skipping test: 'go build' not available on %s/%s", runtime.GOOS, runtime.GOARCH)
+ t.Helper()
+ t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr)
}
}
// runs the test in the directory containing the packaged under test.) That
// means that if we start walking up the tree, we should eventually find
// GOROOT/src/go.mod, and we can report the parent directory of that.
+ //
+ // Notably, this works even if we can't run 'go env GOROOT' as a
+ // subprocess.
cwd, err := os.Getwd()
if err != nil {
// GOROOT reports the path to the directory containing the root of the Go
// project source tree. This is normally equivalent to runtime.GOROOT, but
-// works even if the test binary was built with -trimpath.
+// works even if the test binary was built with -trimpath and cannot exec
+// 'go env GOROOT'.
//
// If GOROOT cannot be found, GOROOT skips t if t is non-nil,
// or panics otherwise.
if !HasGoBuild() {
return "", errors.New("platform cannot run go tool")
}
- return findGoTool()
-}
-
-func findGoTool() (string, error) {
goToolOnce.Do(func() {
- goToolPath, goToolErr = func() (string, error) {
- var exeSuffix string
- if runtime.GOOS == "windows" {
- exeSuffix = ".exe"
- }
- goroot, err := findGOROOT()
- if err != nil {
- return "", fmt.Errorf("cannot find go tool: %w", err)
- }
- path := filepath.Join(goroot, "bin", "go"+exeSuffix)
- if _, err := os.Stat(path); err == nil {
- return path, nil
- }
- goBin, err := exec.LookPath("go" + exeSuffix)
- if err != nil {
- return "", errors.New("cannot find go tool: " + err.Error())
- }
- return goBin, nil
- }()
+ goToolPath, goToolErr = exec.LookPath("go")
})
-
return goToolPath, goToolErr
}
// If not, MustHaveExternalNetwork calls t.Skip with an explanation.
func MustHaveExternalNetwork(t testing.TB) {
if runtime.GOOS == "js" || runtime.GOOS == "wasip1" {
+ t.Helper()
t.Skipf("skipping test: no external network on %s", runtime.GOOS)
}
if testing.Short() {
+ t.Helper()
t.Skipf("skipping test: no external network in -short mode")
}
}
return
}
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
+ cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
panic(fmt.Sprintf("%v: %v", cmd, out))
package testenv_test
import (
+ "internal/platform"
"internal/testenv"
"os"
"path/filepath"
"runtime"
+ "strings"
"testing"
)
t.Fatalf("%q is not the same file as %q", absWant, goTool)
}
}
+
+func TestHasGoBuild(t *testing.T) {
+ if !testenv.HasGoBuild() {
+ switch runtime.GOOS {
+ case "js", "wasip1":
+ // No exec syscall, so these shouldn't be able to 'go build'.
+ t.Logf("HasGoBuild is false on %s", runtime.GOOS)
+ return
+ }
+
+ b := testenv.Builder()
+ if b == "" {
+ // We shouldn't make assumptions about what kind of sandbox or build
+ // environment external Go users may be running in.
+ t.Skipf("skipping: 'go build' unavailable")
+ }
+
+ // Since we control the Go builders, we know which ones ought
+ // to be able to run 'go build'. Check that they can.
+ //
+ // (Note that we don't verify that any builders *can't* run 'go build'.
+ // If a builder starts running 'go build' tests when it shouldn't,
+ // we will presumably find out about it when those tests fail.)
+ switch runtime.GOOS {
+ case "ios":
+ if strings.HasSuffix(b, "-corellium") {
+ // The corellium environment is self-hosting, so it should be able
+ // to build even though real "ios" devices can't exec.
+ } else {
+ // The usual iOS sandbox does not allow the app to start another
+ // process. If we add builders on stock iOS devices, they presumably
+ // will not be able to exec, so we may as well allow that now.
+ t.Logf("HasGoBuild is false on %s", b)
+ return
+ }
+ case "android":
+ if strings.HasSuffix(b, "-emu") && platform.MustLinkExternal(runtime.GOOS, runtime.GOARCH, false) {
+ // As of 2023-05-02, the test environment on the emulated builders is
+ // missing a C linker.
+ t.Logf("HasGoBuild is false on %s", b)
+ return
+ }
+ }
+ t.Fatalf("HasGoBuild unexpectedly false on %s", b)
+ }
+
+ t.Logf("HasGoBuild is true; checking consistency with other functions")
+
+ hasExec := false
+ hasExecGo := false
+ t.Run("MustHaveExec", func(t *testing.T) {
+ testenv.MustHaveExec(t)
+ hasExec = true
+ })
+ t.Run("MustHaveExecPath", func(t *testing.T) {
+ testenv.MustHaveExecPath(t, "go")
+ hasExecGo = true
+ })
+ if !hasExec {
+ t.Errorf(`MustHaveExec(t) skipped unexpectedly`)
+ }
+ if !hasExecGo {
+ t.Errorf(`MustHaveExecPath(t, "go") skipped unexpectedly`)
+ }
+
+ dir := t.TempDir()
+ mainGo := filepath.Join(dir, "main.go")
+ if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0644); err != nil {
+ t.Fatal(err)
+ }
+ cmd := testenv.Command(t, "go", "build", "-o", os.DevNull, mainGo)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("%v: %v\n%s", cmd, err, out)
+ }
+}
+
+func TestMustHaveExec(t *testing.T) {
+ hasExec := false
+ t.Run("MustHaveExec", func(t *testing.T) {
+ testenv.MustHaveExec(t)
+ t.Logf("MustHaveExec did not skip")
+ hasExec = true
+ })
+
+ switch runtime.GOOS {
+ case "js", "wasip1":
+ if hasExec {
+ // js and wasip1 lack an “exec” syscall.
+ t.Errorf("expected MustHaveExec to skip on %v", runtime.GOOS)
+ }
+ case "ios":
+ if b := testenv.Builder(); strings.HasSuffix(b, "-corellium") && !hasExec {
+ // Most ios environments can't exec, but the corellium builder can.
+ t.Errorf("expected MustHaveExec not to skip on %v", b)
+ }
+ default:
+ if b := testenv.Builder(); b != "" && !hasExec {
+ t.Errorf("expected MustHaveExec not to skip on %v", b)
+ }
+ }
+}