golang.org/x/tools@v0.21.1-0.20240520172518-788d39e776b1/internal/testenv/testenv.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package testenv contains helper functions for skipping tests 6 // based on which tools are present in the environment. 7 package testenv 8 9 import ( 10 "bytes" 11 "fmt" 12 "go/build" 13 "os" 14 "os/exec" 15 "path/filepath" 16 "runtime" 17 "runtime/debug" 18 "strings" 19 "sync" 20 "testing" 21 "time" 22 23 "golang.org/x/mod/modfile" 24 "golang.org/x/tools/internal/goroot" 25 ) 26 27 // packageMainIsDevel reports whether the module containing package main 28 // is a development version (if module information is available). 29 func packageMainIsDevel() bool { 30 info, ok := debug.ReadBuildInfo() 31 if !ok { 32 // Most test binaries currently lack build info, but this should become more 33 // permissive once https://golang.org/issue/33976 is fixed. 34 return true 35 } 36 37 // Note: info.Main.Version describes the version of the module containing 38 // package main, not the version of “the main module”. 39 // See https://golang.org/issue/33975. 40 return info.Main.Version == "(devel)" 41 } 42 43 var checkGoBuild struct { 44 once sync.Once 45 err error 46 } 47 48 // HasTool reports an error if the required tool is not available in PATH. 49 // 50 // For certain tools, it checks that the tool executable is correct. 51 func HasTool(tool string) error { 52 if tool == "cgo" { 53 enabled, err := cgoEnabled(false) 54 if err != nil { 55 return fmt.Errorf("checking cgo: %v", err) 56 } 57 if !enabled { 58 return fmt.Errorf("cgo not enabled") 59 } 60 return nil 61 } 62 63 _, err := exec.LookPath(tool) 64 if err != nil { 65 return err 66 } 67 68 switch tool { 69 case "patch": 70 // check that the patch tools supports the -o argument 71 temp, err := os.CreateTemp("", "patch-test") 72 if err != nil { 73 return err 74 } 75 temp.Close() 76 defer os.Remove(temp.Name()) 77 cmd := exec.Command(tool, "-o", temp.Name()) 78 if err := cmd.Run(); err != nil { 79 return err 80 } 81 82 case "go": 83 checkGoBuild.once.Do(func() { 84 if runtime.GOROOT() != "" { 85 // Ensure that the 'go' command found by exec.LookPath is from the correct 86 // GOROOT. Otherwise, 'some/path/go test ./...' will test against some 87 // version of the 'go' binary other than 'some/path/go', which is almost 88 // certainly not what the user intended. 89 out, err := exec.Command(tool, "env", "GOROOT").Output() 90 if err != nil { 91 if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { 92 err = fmt.Errorf("%w\nstderr:\n%s)", err, exit.Stderr) 93 } 94 checkGoBuild.err = err 95 return 96 } 97 GOROOT := strings.TrimSpace(string(out)) 98 if GOROOT != runtime.GOROOT() { 99 checkGoBuild.err = fmt.Errorf("'go env GOROOT' does not match runtime.GOROOT:\n\tgo env: %s\n\tGOROOT: %s", GOROOT, runtime.GOROOT()) 100 return 101 } 102 } 103 104 dir, err := os.MkdirTemp("", "testenv-*") 105 if err != nil { 106 checkGoBuild.err = err 107 return 108 } 109 defer os.RemoveAll(dir) 110 111 mainGo := filepath.Join(dir, "main.go") 112 if err := os.WriteFile(mainGo, []byte("package main\nfunc main() {}\n"), 0644); err != nil { 113 checkGoBuild.err = err 114 return 115 } 116 cmd := exec.Command("go", "build", "-o", os.DevNull, mainGo) 117 cmd.Dir = dir 118 if out, err := cmd.CombinedOutput(); err != nil { 119 if len(out) > 0 { 120 checkGoBuild.err = fmt.Errorf("%v: %v\n%s", cmd, err, out) 121 } else { 122 checkGoBuild.err = fmt.Errorf("%v: %v", cmd, err) 123 } 124 } 125 }) 126 if checkGoBuild.err != nil { 127 return checkGoBuild.err 128 } 129 130 case "diff": 131 // Check that diff is the GNU version, needed for the -u argument and 132 // to report missing newlines at the end of files. 133 out, err := exec.Command(tool, "-version").Output() 134 if err != nil { 135 return err 136 } 137 if !bytes.Contains(out, []byte("GNU diffutils")) { 138 return fmt.Errorf("diff is not the GNU version") 139 } 140 } 141 142 return nil 143 } 144 145 func cgoEnabled(bypassEnvironment bool) (bool, error) { 146 cmd := exec.Command("go", "env", "CGO_ENABLED") 147 if bypassEnvironment { 148 cmd.Env = append(append([]string(nil), os.Environ()...), "CGO_ENABLED=") 149 } 150 out, err := cmd.Output() 151 if err != nil { 152 if exit, ok := err.(*exec.ExitError); ok && len(exit.Stderr) > 0 { 153 err = fmt.Errorf("%w\nstderr:\n%s", err, exit.Stderr) 154 } 155 return false, err 156 } 157 enabled := strings.TrimSpace(string(out)) 158 return enabled == "1", nil 159 } 160 161 func allowMissingTool(tool string) bool { 162 switch runtime.GOOS { 163 case "aix", "darwin", "dragonfly", "freebsd", "illumos", "linux", "netbsd", "openbsd", "plan9", "solaris", "windows": 164 // Known non-mobile OS. Expect a reasonably complete environment. 165 default: 166 return true 167 } 168 169 switch tool { 170 case "cgo": 171 if strings.HasSuffix(os.Getenv("GO_BUILDER_NAME"), "-nocgo") { 172 // Explicitly disabled on -nocgo builders. 173 return true 174 } 175 if enabled, err := cgoEnabled(true); err == nil && !enabled { 176 // No platform support. 177 return true 178 } 179 case "go": 180 if os.Getenv("GO_BUILDER_NAME") == "illumos-amd64-joyent" { 181 // Work around a misconfigured builder (see https://golang.org/issue/33950). 182 return true 183 } 184 case "diff": 185 if os.Getenv("GO_BUILDER_NAME") != "" { 186 return true 187 } 188 case "patch": 189 if os.Getenv("GO_BUILDER_NAME") != "" { 190 return true 191 } 192 } 193 194 // If a developer is actively working on this test, we expect them to have all 195 // of its dependencies installed. However, if it's just a dependency of some 196 // other module (for example, being run via 'go test all'), we should be more 197 // tolerant of unusual environments. 198 return !packageMainIsDevel() 199 } 200 201 // NeedsTool skips t if the named tool is not present in the path. 202 // As a special case, "cgo" means "go" is present and can compile cgo programs. 203 func NeedsTool(t testing.TB, tool string) { 204 err := HasTool(tool) 205 if err == nil { 206 return 207 } 208 209 t.Helper() 210 if allowMissingTool(tool) { 211 // TODO(adonovan): if we skip because of (e.g.) 212 // mismatched go env GOROOT and runtime.GOROOT, don't 213 // we risk some users not getting the coverage they expect? 214 // bcmills notes: this shouldn't be a concern as of CL 404134 (Go 1.19). 215 // We could probably safely get rid of that GOPATH consistency 216 // check entirely at this point. 217 t.Skipf("skipping because %s tool not available: %v", tool, err) 218 } else { 219 t.Fatalf("%s tool not available: %v", tool, err) 220 } 221 } 222 223 // NeedsGoPackages skips t if the go/packages driver (or 'go' tool) implied by 224 // the current process environment is not present in the path. 225 func NeedsGoPackages(t testing.TB) { 226 t.Helper() 227 228 tool := os.Getenv("GOPACKAGESDRIVER") 229 switch tool { 230 case "off": 231 // "off" forces go/packages to use the go command. 232 tool = "go" 233 case "": 234 if _, err := exec.LookPath("gopackagesdriver"); err == nil { 235 tool = "gopackagesdriver" 236 } else { 237 tool = "go" 238 } 239 } 240 241 NeedsTool(t, tool) 242 } 243 244 // NeedsGoPackagesEnv skips t if the go/packages driver (or 'go' tool) implied 245 // by env is not present in the path. 246 func NeedsGoPackagesEnv(t testing.TB, env []string) { 247 t.Helper() 248 249 for _, v := range env { 250 if strings.HasPrefix(v, "GOPACKAGESDRIVER=") { 251 tool := strings.TrimPrefix(v, "GOPACKAGESDRIVER=") 252 if tool == "off" { 253 NeedsTool(t, "go") 254 } else { 255 NeedsTool(t, tool) 256 } 257 return 258 } 259 } 260 261 NeedsGoPackages(t) 262 } 263 264 // NeedsGoBuild skips t if the current system can't build programs with “go build” 265 // and then run them with os.StartProcess or exec.Command. 266 // Android doesn't have the userspace go build needs to run, 267 // and js/wasm doesn't support running subprocesses. 268 func NeedsGoBuild(t testing.TB) { 269 t.Helper() 270 271 // This logic was derived from internal/testing.HasGoBuild and 272 // may need to be updated as that function evolves. 273 274 NeedsTool(t, "go") 275 } 276 277 // ExitIfSmallMachine emits a helpful diagnostic and calls os.Exit(0) if the 278 // current machine is a builder known to have scarce resources. 279 // 280 // It should be called from within a TestMain function. 281 func ExitIfSmallMachine() { 282 switch b := os.Getenv("GO_BUILDER_NAME"); b { 283 case "linux-arm-scaleway": 284 // "linux-arm" was renamed to "linux-arm-scaleway" in CL 303230. 285 fmt.Fprintln(os.Stderr, "skipping test: linux-arm-scaleway builder lacks sufficient memory (https://golang.org/issue/32834)") 286 case "plan9-arm": 287 fmt.Fprintln(os.Stderr, "skipping test: plan9-arm builder lacks sufficient memory (https://golang.org/issue/38772)") 288 case "netbsd-arm-bsiegert", "netbsd-arm64-bsiegert": 289 // As of 2021-06-02, these builders are running with GO_TEST_TIMEOUT_SCALE=10, 290 // and there is only one of each. We shouldn't waste those scarce resources 291 // running very slow tests. 292 fmt.Fprintf(os.Stderr, "skipping test: %s builder is very slow\n", b) 293 case "dragonfly-amd64": 294 // As of 2021-11-02, this builder is running with GO_TEST_TIMEOUT_SCALE=2, 295 // and seems to have unusually slow disk performance. 296 fmt.Fprintln(os.Stderr, "skipping test: dragonfly-amd64 has slow disk (https://golang.org/issue/45216)") 297 case "linux-riscv64-unmatched": 298 // As of 2021-11-03, this builder is empirically not fast enough to run 299 // gopls tests. Ideally we should make the tests faster in short mode 300 // and/or fix them to not assume arbitrary deadlines. 301 // For now, we'll skip them instead. 302 fmt.Fprintf(os.Stderr, "skipping test: %s builder is too slow (https://golang.org/issue/49321)\n", b) 303 default: 304 switch runtime.GOOS { 305 case "android", "ios": 306 fmt.Fprintf(os.Stderr, "skipping test: assuming that %s is resource-constrained\n", runtime.GOOS) 307 default: 308 return 309 } 310 } 311 os.Exit(0) 312 } 313 314 // Go1Point returns the x in Go 1.x. 315 func Go1Point() int { 316 for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- { 317 var version int 318 if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil { 319 continue 320 } 321 return version 322 } 323 panic("bad release tags") 324 } 325 326 // NeedsGo1Point skips t if the Go version used to run the test is older than 327 // 1.x. 328 func NeedsGo1Point(t testing.TB, x int) { 329 if Go1Point() < x { 330 t.Helper() 331 t.Skipf("running Go version %q is version 1.%d, older than required 1.%d", runtime.Version(), Go1Point(), x) 332 } 333 } 334 335 // SkipAfterGo1Point skips t if the Go version used to run the test is newer than 336 // 1.x. 337 func SkipAfterGo1Point(t testing.TB, x int) { 338 if Go1Point() > x { 339 t.Helper() 340 t.Skipf("running Go version %q is version 1.%d, newer than maximum 1.%d", runtime.Version(), Go1Point(), x) 341 } 342 } 343 344 // NeedsLocalhostNet skips t if networking does not work for ports opened 345 // with "localhost". 346 func NeedsLocalhostNet(t testing.TB) { 347 switch runtime.GOOS { 348 case "js", "wasip1": 349 t.Skipf(`Listening on "localhost" fails on %s; see https://go.dev/issue/59718`, runtime.GOOS) 350 } 351 } 352 353 // Deadline returns the deadline of t, if known, 354 // using the Deadline method added in Go 1.15. 355 func Deadline(t testing.TB) (time.Time, bool) { 356 td, ok := t.(interface { 357 Deadline() (time.Time, bool) 358 }) 359 if !ok { 360 return time.Time{}, false 361 } 362 return td.Deadline() 363 } 364 365 // WriteImportcfg writes an importcfg file used by the compiler or linker to 366 // dstPath containing entries for the packages in std and cmd in addition 367 // to the package to package file mappings in additionalPackageFiles. 368 func WriteImportcfg(t testing.TB, dstPath string, additionalPackageFiles map[string]string) { 369 importcfg, err := goroot.Importcfg() 370 for k, v := range additionalPackageFiles { 371 importcfg += fmt.Sprintf("\npackagefile %s=%s", k, v) 372 } 373 if err != nil { 374 t.Fatalf("preparing the importcfg failed: %s", err) 375 } 376 os.WriteFile(dstPath, []byte(importcfg), 0655) 377 if err != nil { 378 t.Fatalf("writing the importcfg failed: %s", err) 379 } 380 } 381 382 var ( 383 gorootOnce sync.Once 384 gorootPath string 385 gorootErr error 386 ) 387 388 func findGOROOT() (string, error) { 389 gorootOnce.Do(func() { 390 gorootPath = runtime.GOROOT() 391 if gorootPath != "" { 392 // If runtime.GOROOT() is non-empty, assume that it is valid. (It might 393 // not be: for example, the user may have explicitly set GOROOT 394 // to the wrong directory.) 395 return 396 } 397 398 cmd := exec.Command("go", "env", "GOROOT") 399 out, err := cmd.Output() 400 if err != nil { 401 gorootErr = fmt.Errorf("%v: %v", cmd, err) 402 } 403 gorootPath = strings.TrimSpace(string(out)) 404 }) 405 406 return gorootPath, gorootErr 407 } 408 409 // GOROOT reports the path to the directory containing the root of the Go 410 // project source tree. This is normally equivalent to runtime.GOROOT, but 411 // works even if the test binary was built with -trimpath. 412 // 413 // If GOROOT cannot be found, GOROOT skips t if t is non-nil, 414 // or panics otherwise. 415 func GOROOT(t testing.TB) string { 416 path, err := findGOROOT() 417 if err != nil { 418 if t == nil { 419 panic(err) 420 } 421 t.Helper() 422 t.Skip(err) 423 } 424 return path 425 } 426 427 // NeedsLocalXTools skips t if the golang.org/x/tools module is replaced and 428 // its replacement directory does not exist (or does not contain the module). 429 func NeedsLocalXTools(t testing.TB) { 430 t.Helper() 431 432 NeedsTool(t, "go") 433 434 cmd := Command(t, "go", "list", "-f", "{{with .Replace}}{{.Dir}}{{end}}", "-m", "golang.org/x/tools") 435 out, err := cmd.Output() 436 if err != nil { 437 if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 { 438 t.Skipf("skipping test: %v: %v\n%s", cmd, err, ee.Stderr) 439 } 440 t.Skipf("skipping test: %v: %v", cmd, err) 441 } 442 443 dir := string(bytes.TrimSpace(out)) 444 if dir == "" { 445 // No replacement directory, and (since we didn't set -e) no error either. 446 // Maybe x/tools isn't replaced at all (as in a gopls release, or when 447 // using a go.work file that includes the x/tools module). 448 return 449 } 450 451 // We found the directory where x/tools would exist if we're in a clone of the 452 // repo. Is it there? (If not, we're probably in the module cache instead.) 453 modFilePath := filepath.Join(dir, "go.mod") 454 b, err := os.ReadFile(modFilePath) 455 if err != nil { 456 t.Skipf("skipping test: x/tools replacement not found: %v", err) 457 } 458 modulePath := modfile.ModulePath(b) 459 460 if want := "golang.org/x/tools"; modulePath != want { 461 t.Skipf("skipping test: %s module path is %q, not %q", modFilePath, modulePath, want) 462 } 463 } 464 465 // NeedsGoExperiment skips t if the current process environment does not 466 // have a GOEXPERIMENT flag set. 467 func NeedsGoExperiment(t testing.TB, flag string) { 468 t.Helper() 469 470 goexp := os.Getenv("GOEXPERIMENT") 471 set := false 472 for _, f := range strings.Split(goexp, ",") { 473 if f == "" { 474 continue 475 } 476 if f == "none" { 477 // GOEXPERIMENT=none disables all experiment flags. 478 set = false 479 break 480 } 481 val := true 482 if strings.HasPrefix(f, "no") { 483 f, val = f[2:], false 484 } 485 if f == flag { 486 set = val 487 } 488 } 489 if !set { 490 t.Skipf("skipping test: flag %q is not set in GOEXPERIMENT=%q", flag, goexp) 491 } 492 }