// 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 ( "cmd/go/internal/base" "cmd/go/internal/cfg" "cmd/go/internal/cmdflag" "cmd/go/internal/work" "errors" "flag" "fmt" "os" "path/filepath" "strconv" "strings" "time" ) //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(&testFuzz, "fuzz", "", "") cf.StringVar(&testList, "list", "", "") cf.StringVar(&testMemProfile, "memprofile", "", "") cf.String("memprofilerate", "", "") cf.StringVar(&testMutexProfile, "mutexprofile", "", "") cf.String("mutexprofilefraction", "", "") cf.Var(&testOutputDir, "outputdir", "") cf.Int("parallel", 0, "") cf.String("run", "", "") cf.Bool("short", false, "") cf.DurationVar(&testTimeout, "timeout", 10*time.Minute, "") cf.String("fuzztime", "", "") cf.String("fuzzminimizetime", "", "") cf.StringVar(&testTrace, "trace", "", "") cf.BoolVar(&testV, "v", false, "") cf.Var(&testShuffle, "shuffle", "") 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 { abs string } func (f *outputdirFlag) String() string { return f.abs } func (f *outputdirFlag) Set(value string) (err error) { if value == "" { f.abs = "" } else { f.abs, err = filepath.Abs(value) } return err } func (f *outputdirFlag) getAbs() string { if f.abs == "" { return base.Cwd() } return f.abs } // vetFlag implements the special parsing logic for the -vet flag: // a comma-separated list, with distinguished values "all" and // "off", plus a boolean tracking whether it was set explicitly. // // "all" is encoded as vetFlag{true, false, nil}, since it will // pass no flags to the vet binary, and by default, it runs all // analyzers. type vetFlag struct { explicit bool off bool flags []string // passed to vet when invoked automatically during 'go test' } func (f *vetFlag) String() string { switch { case !f.off && !f.explicit && len(f.flags) == 0: return "all" case 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 { switch { case value == "": *f = vetFlag{flags: defaultVetFlags} return nil case strings.Contains(value, "="): return fmt.Errorf("-vet argument cannot contain equal signs") case strings.Contains(value, " "): return fmt.Errorf("-vet argument is comma-separated list, cannot contain spaces") } *f = vetFlag{explicit: true} var single string for _, arg := range strings.Split(value, ",") { switch arg { case "": return fmt.Errorf("-vet argument contains empty list element") case "all": single = arg *f = vetFlag{explicit: true} continue case "off": single = arg *f = vetFlag{ explicit: true, off: true, } continue default: if _, ok := passAnalyzersToVet[arg]; !ok { return fmt.Errorf("-vet argument must be a supported analyzer or a distinguished value; found %s", arg) } f.flags = append(f.flags, "-"+arg) } } if len(f.flags) > 1 && single != "" { return fmt.Errorf("-vet does not accept %q in a list with other analyzers", single) } if len(f.flags) > 1 && single != "" { return fmt.Errorf("-vet does not accept %q in a list with other analyzers", single) } return nil } type shuffleFlag struct { on bool seed *int64 } func (f *shuffleFlag) String() string { if !f.on { return "off" } if f.seed == nil { return "on" } return fmt.Sprintf("%d", *f.seed) } func (f *shuffleFlag) Set(value string) error { if value == "off" { *f = shuffleFlag{on: false} return nil } if value == "on" { *f = shuffleFlag{on: true} return nil } seed, err := strconv.ParseInt(value, 10, 64) if err != nil { return fmt.Errorf(`-shuffle argument must be "on", "off", or an int64: %v`, err) } *f = shuffleFlag{on: true, seed: &seed} 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 } }) // firstUnknownFlag helps us report an error when flags not known to 'go // test' are used along with -i or -c. firstUnknownFlag := "" 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 } if firstUnknownFlag == "" { firstUnknownFlag = nd.RawArg } 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 } if firstUnknownFlag != "" && (testC || cfg.BuildI) { buildFlag := "-c" if !testC { buildFlag = "-i" } fmt.Fprintf(os.Stderr, "go: unknown flag %s cannot be used with %s\n", firstUnknownFlag, buildFlag) exitWithUsage() } 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.getAbs()) } // 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() }