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