internal/testenv: use sync.OnceValue[s]

Modernize the code to use sync.OnceValue[s] instead of sync.Once.

While at it, reuse the result of exec.LookPath("go") in tryGoBuild.

Change-Id: I13eff3dd55797846680e506fffb7c49c8296829d
Reviewed-on: https://go-review.googlesource.com/c/go/+/609796
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
Commit-Queue: Ian Lance Taylor <iant@golang.org>
Reviewed-by: Ian Lance Taylor <iant@google.com>
This commit is contained in:
Kir Kolyshkin 2024-08-29 17:31:03 -07:00 committed by Gopher Robot
parent 46985f4ec2
commit b648cd620f

View File

@ -53,63 +53,59 @@ func HasGoBuild() bool {
return false
}
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 {
goBuildErr = fmt.Errorf("%v: %w", cmd, err)
return
}
out = bytes.TrimSpace(out)
if len(out) == 0 {
goBuildErr = fmt.Errorf("%v: no tool reported", cmd)
return
}
if _, err := exec.LookPath(string(out)); err != nil {
goBuildErr = err
return
}
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 goBuildErr == nil
return tryGoBuild() == nil
}
var (
goBuildOnce sync.Once
goBuildErr error
)
var tryGoBuild = sync.OnceValue(func() error {
// 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.)
goTool, err := goTool()
if err != nil {
return err
}
cmd := exec.Command(goTool, "tool", "-n", "compile")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("%v: %w", cmd, err)
}
out = bytes.TrimSpace(out)
if len(out) == 0 {
return fmt.Errorf("%v: no tool reported", cmd)
}
if _, err := exec.LookPath(string(out)); err != nil {
return err
}
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(goTool, "env", "CC")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("%v: %w", cmd, err)
}
out = bytes.TrimSpace(out)
if len(out) == 0 {
return fmt.Errorf("%v: no CC reported", cmd)
}
_, err = exec.LookPath(string(out))
return err
}
}
return nil
})
// MustHaveGoBuild checks that the current system can build programs with “go build”
// and then run them with os.StartProcess or exec.Command.
@ -121,7 +117,7 @@ func MustHaveGoBuild(t testing.TB) {
}
if !HasGoBuild() {
t.Helper()
t.Skipf("skipping test: 'go build' unavailable: %v", goBuildErr)
t.Skipf("skipping test: 'go build' unavailable: %v", tryGoBuild())
}
}
@ -177,82 +173,67 @@ func GoToolPath(t testing.TB) string {
return path
}
var (
gorootOnce sync.Once
gorootPath string
gorootErr error
)
var findGOROOT = sync.OnceValues(func() (path string, err error) {
if path := runtime.GOROOT(); path != "" {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory. But this case is
// rare, and if that happens the user can fix what they broke.)
return path, nil
}
func findGOROOT() (string, error) {
gorootOnce.Do(func() {
gorootPath = runtime.GOROOT()
if gorootPath != "" {
// If runtime.GOROOT() is non-empty, assume that it is valid.
//
// (It might not be: for example, the user may have explicitly set GOROOT
// to the wrong directory. But this case is
// rare, and if that happens the user can fix what they broke.)
return
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
// 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 {
return "", fmt.Errorf("finding GOROOT: %w", err)
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
return "", fmt.Errorf("failed to locate GOROOT/src in any parent directory")
}
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
// binary was built with -trimpath).
//
// Since this is internal/testenv, we can cheat and assume that the caller
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
// 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.
if base := filepath.Base(dir); base != "src" {
dir = parent
continue // dir cannot be GOROOT/src if it doesn't end in "src".
}
cwd, err := os.Getwd()
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
dir := cwd
for {
parent := filepath.Dir(dir)
if parent == dir {
// dir is either "." or only a volume name.
gorootErr = fmt.Errorf("failed to locate GOROOT/src in any parent directory")
return
}
if base := filepath.Base(dir); base != "src" {
if os.IsNotExist(err) {
dir = parent
continue // dir cannot be GOROOT/src if it doesn't end in "src".
continue
}
return "", fmt.Errorf("finding GOROOT: %w", err)
}
goMod := string(b)
b, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil {
if os.IsNotExist(err) {
dir = parent
continue
}
gorootErr = fmt.Errorf("finding GOROOT: %w", err)
return
}
goMod := string(b)
for goMod != "" {
var line string
line, goMod, _ = strings.Cut(goMod, "\n")
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
// Found "module std", which is the module declaration in GOROOT/src!
gorootPath = parent
return
}
for goMod != "" {
var line string
line, goMod, _ = strings.Cut(goMod, "\n")
fields := strings.Fields(line)
if len(fields) >= 2 && fields[0] == "module" && fields[1] == "std" {
// Found "module std", which is the module declaration in GOROOT/src!
return parent, nil
}
}
})
return gorootPath, gorootErr
}
}
})
// GOROOT reports the path to the directory containing the root of the Go
// project source tree. This is normally equivalent to runtime.GOROOT, but
@ -278,17 +259,12 @@ func GoTool() (string, error) {
if !HasGoBuild() {
return "", errors.New("platform cannot run go tool")
}
goToolOnce.Do(func() {
goToolPath, goToolErr = exec.LookPath("go")
})
return goToolPath, goToolErr
return goTool()
}
var (
goToolOnce sync.Once
goToolPath string
goToolErr error
)
var goTool = sync.OnceValues(func() (string, error) {
return exec.LookPath("go")
})
// HasSrc reports whether the entire source tree is available under GOROOT.
func HasSrc() bool {
@ -321,29 +297,26 @@ func MustHaveExternalNetwork(t testing.TB) {
// HasCGO reports whether the current system can use cgo.
func HasCGO() bool {
hasCgoOnce.Do(func() {
goTool, err := GoTool()
if err != nil {
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))
}
hasCgo, err = strconv.ParseBool(string(bytes.TrimSpace(out)))
if err != nil {
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
}
})
return hasCgo
return hasCgo()
}
var (
hasCgoOnce sync.Once
hasCgo bool
)
var hasCgo = sync.OnceValue(func() bool {
goTool, err := goTool()
if err != nil {
return false
}
cmd := exec.Command(goTool, "env", "CGO_ENABLED")
cmd.Env = origEnv
out, err := cmd.Output()
if err != nil {
panic(fmt.Sprintf("%v: %v", cmd, out))
}
ok, err := strconv.ParseBool(string(bytes.TrimSpace(out)))
if err != nil {
panic(fmt.Sprintf("%v: non-boolean output %q", cmd, out))
}
return ok
})
// MustHaveCGO calls t.Skip if cgo is not available.
func MustHaveCGO(t testing.TB) {