github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/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 "runtime" 15 "strings" 16 "sync" 17 "time" 18 19 exec "golang.org/x/sys/execabs" 20 ) 21 22 // Testing is an abstraction of a *testing.T. 23 type Testing interface { 24 Skipf(format string, args ...interface{}) 25 Fatalf(format string, args ...interface{}) 26 } 27 28 type helperer interface { 29 Helper() 30 } 31 32 // packageMainIsDevel reports whether the module containing package main 33 // is a development version (if module information is available). 34 // 35 // Builds in GOPATH mode and builds that lack module information are assumed to 36 // be development versions. 37 var packageMainIsDevel = func() bool { return true } 38 39 var checkGoGoroot struct { 40 once sync.Once 41 err error 42 } 43 44 func hasTool(tool string) error { 45 if tool == "cgo" { 46 enabled, err := cgoEnabled(false) 47 if err != nil { 48 return fmt.Errorf("checking cgo: %v", err) 49 } 50 if !enabled { 51 return fmt.Errorf("cgo not enabled") 52 } 53 return nil 54 } 55 56 _, err := exec.LookPath(tool) 57 if err != nil { 58 return err 59 } 60 61 switch tool { 62 case "patch": 63 // check that the patch tools supports the -o argument 64 temp, err := os.CreateTemp("", "patch-test") 65 if err != nil { 66 return err 67 } 68 temp.Close() 69 defer os.Remove(temp.Name()) 70 cmd := exec.Command(tool, "-o", temp.Name()) 71 if err := cmd.Run(); err != nil { 72 return err 73 } 74 75 case "go": 76 checkGoGoroot.once.Do(func() { 77 // Ensure that the 'go' command found by exec.LookPath is from the correct 78 // GOROOT. Otherwise, 'some/path/go test ./...' will test against some 79 // version of the 'go' binary other than 'some/path/go', which is almost 80 // certainly not what the user intended. 81 out, err := exec.Command(tool, "env", "GOROOT").CombinedOutput() 82 if err != nil { 83 checkGoGoroot.err = err 84 return 85 } 86 GOROOT := strings.TrimSpace(string(out)) 87 if GOROOT != runtime.GOROOT() { 88 checkGoGoroot.err = fmt.Errorf("'go env GOROOT' does not match runtime.GOROOT:\n\tgo env: %s\n\tGOROOT: %s", GOROOT, runtime.GOROOT()) 89 } 90 }) 91 if checkGoGoroot.err != nil { 92 return checkGoGoroot.err 93 } 94 95 case "diff": 96 // Check that diff is the GNU version, needed for the -u argument and 97 // to report missing newlines at the end of files. 98 out, err := exec.Command(tool, "-version").Output() 99 if err != nil { 100 return err 101 } 102 if !bytes.Contains(out, []byte("GNU diffutils")) { 103 return fmt.Errorf("diff is not the GNU version") 104 } 105 } 106 107 return nil 108 } 109 110 func cgoEnabled(bypassEnvironment bool) (bool, error) { 111 cmd := exec.Command("go", "env", "CGO_ENABLED") 112 if bypassEnvironment { 113 cmd.Env = append(append([]string(nil), os.Environ()...), "CGO_ENABLED=") 114 } 115 out, err := cmd.CombinedOutput() 116 if err != nil { 117 return false, err 118 } 119 enabled := strings.TrimSpace(string(out)) 120 return enabled == "1", nil 121 } 122 123 func allowMissingTool(tool string) bool { 124 if runtime.GOOS == "android" { 125 // Android builds generally run tests on a separate machine from the build, 126 // so don't expect any external tools to be available. 127 return true 128 } 129 130 switch tool { 131 case "cgo": 132 if strings.HasSuffix(os.Getenv("GO_BUILDER_NAME"), "-nocgo") { 133 // Explicitly disabled on -nocgo builders. 134 return true 135 } 136 if enabled, err := cgoEnabled(true); err == nil && !enabled { 137 // No platform support. 138 return true 139 } 140 case "go": 141 if os.Getenv("GO_BUILDER_NAME") == "illumos-amd64-joyent" { 142 // Work around a misconfigured builder (see https://golang.org/issue/33950). 143 return true 144 } 145 case "diff": 146 if os.Getenv("GO_BUILDER_NAME") != "" { 147 return true 148 } 149 case "patch": 150 if os.Getenv("GO_BUILDER_NAME") != "" { 151 return true 152 } 153 } 154 155 // If a developer is actively working on this test, we expect them to have all 156 // of its dependencies installed. However, if it's just a dependency of some 157 // other module (for example, being run via 'go test all'), we should be more 158 // tolerant of unusual environments. 159 return !packageMainIsDevel() 160 } 161 162 // NeedsTool skips t if the named tool is not present in the path. 163 // As a special case, "cgo" means "go" is present and can compile cgo programs. 164 func NeedsTool(t Testing, tool string) { 165 if t, ok := t.(helperer); ok { 166 t.Helper() 167 } 168 err := hasTool(tool) 169 if err == nil { 170 return 171 } 172 if allowMissingTool(tool) { 173 t.Skipf("skipping because %s tool not available: %v", tool, err) 174 } else { 175 t.Fatalf("%s tool not available: %v", tool, err) 176 } 177 } 178 179 // NeedsGoPackages skips t if the go/packages driver (or 'go' tool) implied by 180 // the current process environment is not present in the path. 181 func NeedsGoPackages(t Testing) { 182 if t, ok := t.(helperer); ok { 183 t.Helper() 184 } 185 186 tool := os.Getenv("GOPACKAGESDRIVER") 187 switch tool { 188 case "off": 189 // "off" forces go/packages to use the go command. 190 tool = "go" 191 case "": 192 if _, err := exec.LookPath("gopackagesdriver"); err == nil { 193 tool = "gopackagesdriver" 194 } else { 195 tool = "go" 196 } 197 } 198 199 NeedsTool(t, tool) 200 } 201 202 // NeedsGoPackagesEnv skips t if the go/packages driver (or 'go' tool) implied 203 // by env is not present in the path. 204 func NeedsGoPackagesEnv(t Testing, env []string) { 205 if t, ok := t.(helperer); ok { 206 t.Helper() 207 } 208 209 for _, v := range env { 210 if strings.HasPrefix(v, "GOPACKAGESDRIVER=") { 211 tool := strings.TrimPrefix(v, "GOPACKAGESDRIVER=") 212 if tool == "off" { 213 NeedsTool(t, "go") 214 } else { 215 NeedsTool(t, tool) 216 } 217 return 218 } 219 } 220 221 NeedsGoPackages(t) 222 } 223 224 // NeedsGoBuild skips t if the current system can't build programs with “go build” 225 // and then run them with os.StartProcess or exec.Command. 226 // android, and darwin/arm systems don't have the userspace go build needs to run, 227 // and js/wasm doesn't support running subprocesses. 228 func NeedsGoBuild(t Testing) { 229 if t, ok := t.(helperer); ok { 230 t.Helper() 231 } 232 233 NeedsTool(t, "go") 234 235 switch runtime.GOOS { 236 case "android", "js": 237 t.Skipf("skipping test: %v can't build and run Go binaries", runtime.GOOS) 238 case "darwin": 239 if strings.HasPrefix(runtime.GOARCH, "arm") { 240 t.Skipf("skipping test: darwin/arm can't build and run Go binaries") 241 } 242 } 243 } 244 245 // ExitIfSmallMachine emits a helpful diagnostic and calls os.Exit(0) if the 246 // current machine is a builder known to have scarce resources. 247 // 248 // It should be called from within a TestMain function. 249 func ExitIfSmallMachine() { 250 switch b := os.Getenv("GO_BUILDER_NAME"); b { 251 case "linux-arm-scaleway": 252 // "linux-arm" was renamed to "linux-arm-scaleway" in CL 303230. 253 fmt.Fprintln(os.Stderr, "skipping test: linux-arm-scaleway builder lacks sufficient memory (https://golang.org/issue/32834)") 254 case "plan9-arm": 255 fmt.Fprintln(os.Stderr, "skipping test: plan9-arm builder lacks sufficient memory (https://golang.org/issue/38772)") 256 case "netbsd-arm-bsiegert", "netbsd-arm64-bsiegert": 257 // As of 2021-06-02, these builders are running with GO_TEST_TIMEOUT_SCALE=10, 258 // and there is only one of each. We shouldn't waste those scarce resources 259 // running very slow tests. 260 fmt.Fprintf(os.Stderr, "skipping test: %s builder is very slow\n", b) 261 case "dragonfly-amd64": 262 // As of 2021-11-02, this builder is running with GO_TEST_TIMEOUT_SCALE=2, 263 // and seems to have unusually slow disk performance. 264 fmt.Fprintln(os.Stderr, "skipping test: dragonfly-amd64 has slow disk (https://golang.org/issue/45216)") 265 case "linux-riscv64-unmatched": 266 // As of 2021-11-03, this builder is empirically not fast enough to run 267 // gopls tests. Ideally we should make the tests faster in short mode 268 // and/or fix them to not assume arbitrary deadlines. 269 // For now, we'll skip them instead. 270 fmt.Fprintf(os.Stderr, "skipping test: %s builder is too slow (https://golang.org/issue/49321)\n", b) 271 default: 272 return 273 } 274 os.Exit(0) 275 } 276 277 // Go1Point returns the x in Go 1.x. 278 func Go1Point() int { 279 for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- { 280 var version int 281 if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil { 282 continue 283 } 284 return version 285 } 286 panic("bad release tags") 287 } 288 289 // NeedsGo1Point skips t if the Go version used to run the test is older than 290 // 1.x. 291 func NeedsGo1Point(t Testing, x int) { 292 if t, ok := t.(helperer); ok { 293 t.Helper() 294 } 295 if Go1Point() < x { 296 t.Skipf("running Go version %q is version 1.%d, older than required 1.%d", runtime.Version(), Go1Point(), x) 297 } 298 } 299 300 // SkipAfterGo1Point skips t if the Go version used to run the test is newer than 301 // 1.x. 302 func SkipAfterGo1Point(t Testing, x int) { 303 if t, ok := t.(helperer); ok { 304 t.Helper() 305 } 306 if Go1Point() > x { 307 t.Skipf("running Go version %q is version 1.%d, newer than maximum 1.%d", runtime.Version(), Go1Point(), x) 308 } 309 } 310 311 // Deadline returns the deadline of t, if known, 312 // using the Deadline method added in Go 1.15. 313 func Deadline(t Testing) (time.Time, bool) { 314 td, ok := t.(interface { 315 Deadline() (time.Time, bool) 316 }) 317 if !ok { 318 return time.Time{}, false 319 } 320 return td.Deadline() 321 }