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