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