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