github.com/rogpeppe/go-internal@v1.12.1-0.20240509064211-c8567cf8e95f/gotooltest/setup.go (about)

     1  // Copyright 2015 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 gotooltest implements functionality useful for testing
     6  // tools that use the go command.
     7  package gotooltest
     8  
     9  import (
    10  	"bytes"
    11  	"encoding/json"
    12  	"fmt"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"regexp"
    17  	"runtime"
    18  	"strings"
    19  	"sync"
    20  
    21  	"github.com/rogpeppe/go-internal/testscript"
    22  )
    23  
    24  var (
    25  	goVersionRegex = regexp.MustCompile(`^go([1-9][0-9]*)\.([1-9][0-9]*)$`)
    26  
    27  	goEnv struct {
    28  		GOROOT      string
    29  		GOCACHE     string
    30  		GOPROXY     string
    31  		goversion   string
    32  		releaseTags []string
    33  		once        sync.Once
    34  		err         error
    35  	}
    36  )
    37  
    38  // initGoEnv initialises goEnv. It should only be called using goEnv.once.Do,
    39  // as in Setup.
    40  //
    41  // Run all of these probe commands in a temporary directory, so as not to make
    42  // any assumptions about the caller's working directory.
    43  func initGoEnv() (err error) {
    44  	td, err := os.MkdirTemp("", "gotooltest-initGoEnv")
    45  	if err != nil {
    46  		return fmt.Errorf("failed to create temporary directory for go command tests: %w", err)
    47  	}
    48  	defer func() {
    49  		if rerr := os.RemoveAll(td); rerr != nil && err == nil {
    50  			err = fmt.Errorf("failed to remove temporary directory for go command tests: %w", rerr)
    51  		}
    52  	}()
    53  
    54  	// Write a temporary go.mod file in td. This ensures that we create
    55  	// a porcelain environment in which to run these probe commands.
    56  	if err := os.WriteFile(filepath.Join(td, "go.mod"), []byte("module gotooltest"), 0600); err != nil {
    57  		return fmt.Errorf("failed to write temporary go.mod file: %w", err)
    58  	}
    59  
    60  	run := func(args ...string) (*bytes.Buffer, *bytes.Buffer, error) {
    61  		var stdout, stderr bytes.Buffer
    62  		cmd := exec.Command(args[0], args[1:]...)
    63  		cmd.Dir = td
    64  		cmd.Stdout = &stdout
    65  		cmd.Stderr = &stderr
    66  		return &stdout, &stderr, cmd.Run()
    67  	}
    68  
    69  	lout, stderr, err := run("go", "list", "-f={{context.ReleaseTags}}", "runtime")
    70  	if err != nil {
    71  		return fmt.Errorf("failed to determine release tags from go command: %v\n%v", err, stderr.String())
    72  	}
    73  	tagStr := strings.TrimSpace(lout.String())
    74  	tagStr = strings.Trim(tagStr, "[]")
    75  	goEnv.releaseTags = strings.Split(tagStr, " ")
    76  
    77  	eout, stderr, err := run("go", "env", "-json",
    78  		"GOROOT",
    79  		"GOCACHE",
    80  		"GOPROXY",
    81  	)
    82  	if err != nil {
    83  		return fmt.Errorf("failed to determine environment from go command: %v\n%v", err, stderr)
    84  	}
    85  	if err := json.Unmarshal(eout.Bytes(), &goEnv); err != nil {
    86  		return fmt.Errorf("failed to unmarshal GOROOT and GOCACHE tags from go command out: %v\n%v", err, eout)
    87  	}
    88  
    89  	version := goEnv.releaseTags[len(goEnv.releaseTags)-1]
    90  	if !goVersionRegex.MatchString(version) {
    91  		return fmt.Errorf("invalid go version %q", version)
    92  	}
    93  	goEnv.goversion = version[2:]
    94  
    95  	return nil
    96  }
    97  
    98  // Setup sets up the given test environment for tests that use the go
    99  // command. It adds support for go tags to p.Condition and adds the go
   100  // command to p.Cmds. It also wraps p.Setup to set up the environment
   101  // variables for running the go command appropriately.
   102  //
   103  // It checks go command can run, but not that it can build or run
   104  // binaries.
   105  func Setup(p *testscript.Params) error {
   106  	goEnv.once.Do(func() {
   107  		goEnv.err = initGoEnv()
   108  	})
   109  	if goEnv.err != nil {
   110  		return goEnv.err
   111  	}
   112  
   113  	origSetup := p.Setup
   114  	p.Setup = func(e *testscript.Env) error {
   115  		e.Vars = goEnviron(e.Vars)
   116  		if origSetup != nil {
   117  			return origSetup(e)
   118  		}
   119  		return nil
   120  	}
   121  	if p.Cmds == nil {
   122  		p.Cmds = make(map[string]func(ts *testscript.TestScript, neg bool, args []string))
   123  	}
   124  	p.Cmds["go"] = cmdGo
   125  	return nil
   126  }
   127  
   128  func goEnviron(env0 []string) []string {
   129  	env := environ(env0)
   130  	workdir := env.get("WORK")
   131  	return append(env, []string{
   132  		"GOPATH=" + filepath.Join(workdir, ".gopath"),
   133  		"CCACHE_DISABLE=1", // ccache breaks with non-existent HOME
   134  		"GOARCH=" + runtime.GOARCH,
   135  		"GOOS=" + runtime.GOOS,
   136  		"GOROOT=" + goEnv.GOROOT,
   137  		"GOCACHE=" + goEnv.GOCACHE,
   138  		"GOPROXY=" + goEnv.GOPROXY,
   139  		"goversion=" + goEnv.goversion,
   140  	}...)
   141  }
   142  
   143  func cmdGo(ts *testscript.TestScript, neg bool, args []string) {
   144  	if len(args) < 1 {
   145  		ts.Fatalf("usage: go subcommand ...")
   146  	}
   147  	err := ts.Exec("go", args...)
   148  	if err != nil {
   149  		ts.Logf("[%v]\n", err)
   150  		if !neg {
   151  			ts.Fatalf("unexpected go command failure")
   152  		}
   153  	} else {
   154  		if neg {
   155  			ts.Fatalf("unexpected go command success")
   156  		}
   157  	}
   158  }
   159  
   160  type environ []string
   161  
   162  func (e0 *environ) get(name string) string {
   163  	e := *e0
   164  	for i := len(e) - 1; i >= 0; i-- {
   165  		v := e[i]
   166  		if len(v) <= len(name) {
   167  			continue
   168  		}
   169  		if strings.HasPrefix(v, name) && v[len(name)] == '=' {
   170  			return v[len(name)+1:]
   171  		}
   172  	}
   173  	return ""
   174  }