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 }