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