blob: 4f0a8924f12c0bde08cf9b37a82717b6b9b6dda5 [file] [log] [blame]
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package test
import (
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"cmd/go/internal/base"
"cmd/go/internal/cfg"
"cmd/go/internal/cmdflag"
"cmd/go/internal/work"
)
//go:generate go run ./genflags.go
// The flag handling part of go test is large and distracting.
// We can't use (*flag.FlagSet).Parse because some of the flags from
// our command line are for us, and some are for the test binary, and
// some are for both.
func init() {
work.AddBuildFlags(CmdTest, work.OmitVFlag)
cf := CmdTest.Flag
cf.BoolVar(&testC, "c", false, "")
cf.BoolVar(&cfg.BuildI, "i", false, "")
cf.StringVar(&testO, "o", "", "")
cf.BoolVar(&testCover, "cover", false, "")
cf.Var(coverFlag{(*coverModeFlag)(&testCoverMode)}, "covermode", "")
cf.Var(coverFlag{commaListFlag{&testCoverPaths}}, "coverpkg", "")
cf.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
cf.BoolVar(&testJSON, "json", false, "")
cf.Var(&testVet, "vet", "")
// Register flags to be forwarded to the test binary. We retain variables for
// some of them so that cmd/go knows what to do with the test output, or knows
// to build the test in a way that supports the use of the flag.
cf.StringVar(&testBench, "bench", "", "")
cf.Bool("benchmem", false, "")
cf.String("benchtime", "", "")
cf.StringVar(&testBlockProfile, "blockprofile", "", "")
cf.String("blockprofilerate", "", "")
cf.Int("count", 0, "")
cf.Var(coverFlag{stringFlag{&testCoverProfile}}, "coverprofile", "")
cf.String("cpu", "", "")
cf.StringVar(&testCPUProfile, "cpuprofile", "", "")
cf.Bool("failfast", false, "")
cf.StringVar(&testList, "list", "", "")
cf.StringVar(&testMemProfile, "memprofile", "", "")
cf.String("memprofilerate", "", "")
cf.StringVar(&testMutexProfile, "mutexprofile", "", "")
cf.String("mutexprofilefraction", "", "")
cf.Var(outputdirFlag{&testOutputDir}, "outputdir", "")
cf.Int("parallel", 0, "")
cf.String("run", "", "")
cf.Bool("short", false, "")
cf.DurationVar(&testTimeout, "timeout", 10*time.Minute, "")
cf.StringVar(&testTrace, "trace", "", "")
cf.BoolVar(&testV, "v", false, "")
for name, _ := range passFlagToTest {
cf.Var(cf.Lookup(name).Value, "test."+name, "")
}
}
// A coverFlag is a flag.Value that also implies -cover.
type coverFlag struct{ v flag.Value }
func (f coverFlag) String() string { return f.v.String() }
func (f coverFlag) Set(value string) error {
if err := f.v.Set(value); err != nil {
return err
}
testCover = true
return nil
}
type coverModeFlag string
func (f *coverModeFlag) String() string { return string(*f) }
func (f *coverModeFlag) Set(value string) error {
switch value {
case "", "set", "count", "atomic":
*f = coverModeFlag(value)
return nil
default:
return errors.New(`valid modes are "set", "count", or "atomic"`)
}
}
// A commaListFlag is a flag.Value representing a comma-separated list.
type commaListFlag struct{ vals *[]string }
func (f commaListFlag) String() string { return strings.Join(*f.vals, ",") }
func (f commaListFlag) Set(value string) error {
if value == "" {
*f.vals = nil
} else {
*f.vals = strings.Split(value, ",")
}
return nil
}
// A stringFlag is a flag.Value representing a single string.
type stringFlag struct{ val *string }
func (f stringFlag) String() string { return *f.val }
func (f stringFlag) Set(value string) error {
*f.val = value
return nil
}
// outputdirFlag implements the -outputdir flag.
// It interprets an empty value as the working directory of the 'go' command.
type outputdirFlag struct {
resolved *string
}
func (f outputdirFlag) String() string { return *f.resolved }
func (f outputdirFlag) Set(value string) (err error) {
if value == "" {
// The empty string implies the working directory of the 'go' command.
*f.resolved = base.Cwd
} else {
*f.resolved, err = filepath.Abs(value)
}
return err
}
// vetFlag implements the special parsing logic for the -vet flag:
// a comma-separated list, with a distinguished value "off" and
// a boolean tracking whether it was set explicitly.
type vetFlag struct {
explicit bool
off bool
flags []string // passed to vet when invoked automatically during 'go test'
}
func (f *vetFlag) String() string {
if f.off {
return "off"
}
var buf strings.Builder
for i, f := range f.flags {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(f)
}
return buf.String()
}
func (f *vetFlag) Set(value string) error {
if value == "" {
*f = vetFlag{flags: defaultVetFlags}
return nil
}
if value == "off" {
*f = vetFlag{
explicit: true,
off: true,
}
return nil
}
if strings.Contains(value, "=") {
return fmt.Errorf("-vet argument cannot contain equal signs")
}
if strings.Contains(value, " ") {
return fmt.Errorf("-vet argument is comma-separated list, cannot contain spaces")
}
*f = vetFlag{explicit: true}
for _, arg := range strings.Split(value, ",") {
if arg == "" {
return fmt.Errorf("-vet argument contains empty list element")
}
f.flags = append(f.flags, "-"+arg)
}
return nil
}
// testFlags processes the command line, grabbing -x and -c, rewriting known flags
// to have "test" before them, and reading the command line for the test binary.
// Unfortunately for us, we need to do our own flag processing because go test
// grabs some flags but otherwise its command line is just a holding place for
// pkg.test's arguments.
// We allow known flags both before and after the package name list,
// to allow both
// go test fmt -custom-flag-for-fmt-test
// go test -x math
func testFlags(args []string) (packageNames, passToTest []string) {
base.SetFromGOFLAGS(&CmdTest.Flag)
addFromGOFLAGS := map[string]bool{}
CmdTest.Flag.Visit(func(f *flag.Flag) {
if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
addFromGOFLAGS[f.Name] = true
}
})
explicitArgs := make([]string, 0, len(args))
inPkgList := false
afterFlagWithoutValue := false
for len(args) > 0 {
f, remainingArgs, err := cmdflag.ParseOne(&CmdTest.Flag, args)
wasAfterFlagWithoutValue := afterFlagWithoutValue
afterFlagWithoutValue = false // provisionally
if errors.Is(err, flag.ErrHelp) {
exitWithUsage()
}
if errors.Is(err, cmdflag.ErrFlagTerminator) {
// 'go list' allows package arguments to be named either before or after
// the terminator, but 'go test' has historically allowed them only
// before. Preserve that behavior and treat all remaining arguments —
// including the terminator itself! — as arguments to the test.
explicitArgs = append(explicitArgs, args...)
break
}
if nf := (cmdflag.NonFlagError{}); errors.As(err, &nf) {
if !inPkgList && packageNames != nil {
// We already saw the package list previously, and this argument is not
// a flag, so it — and everything after it — must be either a value for
// a preceding flag or a literal argument to the test binary.
if wasAfterFlagWithoutValue {
// This argument could syntactically be a flag value, so
// optimistically assume that it is and keep looking for go command
// flags after it.
//
// (If we're wrong, we'll at least be consistent with historical
// behavior; see https://golang.org/issue/40763.)
explicitArgs = append(explicitArgs, nf.RawArg)
args = remainingArgs
continue
} else {
// This argument syntactically cannot be a flag value, so it must be a
// positional argument, and so must everything after it.
explicitArgs = append(explicitArgs, args...)
break
}
}
inPkgList = true
packageNames = append(packageNames, nf.RawArg)
args = remainingArgs // Consume the package name.
continue
}
if inPkgList {
// This argument is syntactically a flag, so if we were in the package
// list we're not anymore.
inPkgList = false
}
if nd := (cmdflag.FlagNotDefinedError{}); errors.As(err, &nd) {
// This is a flag we do not know. We must assume that any args we see
// after this might be flag arguments, not package names, so make
// packageNames non-nil to indicate that the package list is complete.
//
// (Actually, we only strictly need to assume that if the flag is not of
// the form -x=value, but making this more precise would be a breaking
// change in the command line API.)
if packageNames == nil {
packageNames = []string{}
}
if nd.RawArg == "-args" || nd.RawArg == "--args" {
// -args or --args signals that everything that follows
// should be passed to the test.
explicitArgs = append(explicitArgs, remainingArgs...)
break
}
explicitArgs = append(explicitArgs, nd.RawArg)
args = remainingArgs
if !nd.HasValue {
afterFlagWithoutValue = true
}
continue
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
exitWithUsage()
}
if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
explicitArgs = append(explicitArgs, fmt.Sprintf("-test.%s=%v", short, f.Value))
// This flag has been overridden explicitly, so don't forward its implicit
// value from GOFLAGS.
delete(addFromGOFLAGS, short)
delete(addFromGOFLAGS, "test."+short)
}
args = remainingArgs
}
var injectedFlags []string
if testJSON {
// If converting to JSON, we need the full output in order to pipe it to
// test2json.
injectedFlags = append(injectedFlags, "-test.v=true")
delete(addFromGOFLAGS, "v")
delete(addFromGOFLAGS, "test.v")
}
// Inject flags from GOFLAGS before the explicit command-line arguments.
// (They must appear before the flag terminator or first non-flag argument.)
// Also determine whether flags with awkward defaults have already been set.
var timeoutSet, outputDirSet bool
CmdTest.Flag.Visit(func(f *flag.Flag) {
short := strings.TrimPrefix(f.Name, "test.")
if addFromGOFLAGS[f.Name] {
injectedFlags = append(injectedFlags, fmt.Sprintf("-test.%s=%v", short, f.Value))
}
switch short {
case "timeout":
timeoutSet = true
case "outputdir":
outputDirSet = true
}
})
// 'go test' has a default timeout, but the test binary itself does not.
// If the timeout wasn't set (and forwarded) explicitly, add the default
// timeout to the command line.
if testTimeout > 0 && !timeoutSet {
injectedFlags = append(injectedFlags, fmt.Sprintf("-test.timeout=%v", testTimeout))
}
// Similarly, the test binary defaults -test.outputdir to its own working
// directory, but 'go test' defaults it to the working directory of the 'go'
// command. Set it explicitly if it is needed due to some other flag that
// requests output.
if testProfile() != "" && !outputDirSet {
injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir)
}
// If the user is explicitly passing -help or -h, show output
// of the test binary so that the help output is displayed
// even though the test will exit with success.
// This loop is imperfect: it will do the wrong thing for a case
// like -args -test.outputdir -help. Such cases are probably rare,
// and getting this wrong doesn't do too much harm.
helpLoop:
for _, arg := range explicitArgs {
switch arg {
case "--":
break helpLoop
case "-h", "-help", "--help":
testHelp = true
break helpLoop
}
}
// Ensure that -race and -covermode are compatible.
if testCoverMode == "" {
testCoverMode = "set"
if cfg.BuildRace {
// Default coverage mode is atomic when -race is set.
testCoverMode = "atomic"
}
}
if cfg.BuildRace && testCoverMode != "atomic" {
base.Fatalf(`-covermode must be "atomic", not %q, when -race is enabled`, testCoverMode)
}
// Forward any unparsed arguments (following --args) to the test binary.
return packageNames, append(injectedFlags, explicitArgs...)
}
func exitWithUsage() {
fmt.Fprintf(os.Stderr, "usage: %s\n", CmdTest.UsageLine)
fmt.Fprintf(os.Stderr, "Run 'go help %s' and 'go help %s' for details.\n", CmdTest.LongName(), HelpTestflag.LongName())
base.SetExitStatus(2)
base.Exit()
}