Source file src/cmd/go/internal/test/testflag.go

     1  // Copyright 2011 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 test
     6  
     7  import (
     8  	"cmd/go/internal/base"
     9  	"cmd/go/internal/cfg"
    10  	"cmd/go/internal/cmdflag"
    11  	"cmd/go/internal/work"
    12  	"errors"
    13  	"flag"
    14  	"fmt"
    15  	"internal/godebug"
    16  	"os"
    17  	"path/filepath"
    18  	"strconv"
    19  	"strings"
    20  	"time"
    21  )
    22  
    23  //go:generate go run ./genflags.go
    24  
    25  // The flag handling part of go test is large and distracting.
    26  // We can't use (*flag.FlagSet).Parse because some of the flags from
    27  // our command line are for us, and some are for the test binary, and
    28  // some are for both.
    29  
    30  var gotestjsonbuildtext = godebug.New("gotestjsonbuildtext")
    31  
    32  func init() {
    33  	work.AddBuildFlags(CmdTest, work.OmitVFlag|work.OmitJSONFlag)
    34  
    35  	cf := CmdTest.Flag
    36  	cf.BoolVar(&testC, "c", false, "")
    37  	cf.StringVar(&testO, "o", "", "")
    38  	work.AddCoverFlags(CmdTest, &testCoverProfile)
    39  	cf.Var((*base.StringsFlag)(&work.ExecCmd), "exec", "")
    40  	cf.BoolVar(&testJSON, "json", false, "")
    41  	cf.Var(&testVet, "vet", "")
    42  
    43  	// Register flags to be forwarded to the test binary. We retain variables for
    44  	// some of them so that cmd/go knows what to do with the test output, or knows
    45  	// to build the test in a way that supports the use of the flag.
    46  
    47  	cf.BoolVar(&testArtifacts, "artifacts", false, "")
    48  	cf.StringVar(&testBench, "bench", "", "")
    49  	cf.Bool("benchmem", false, "")
    50  	cf.String("benchtime", "", "")
    51  	cf.StringVar(&testBlockProfile, "blockprofile", "", "")
    52  	cf.String("blockprofilerate", "", "")
    53  	cf.Int("count", 0, "")
    54  	cf.String("cpu", "", "")
    55  	cf.StringVar(&testCPUProfile, "cpuprofile", "", "")
    56  	cf.BoolVar(&testFailFast, "failfast", false, "")
    57  	cf.StringVar(&testFuzz, "fuzz", "", "")
    58  	cf.Bool("fullpath", false, "")
    59  	cf.StringVar(&testList, "list", "", "")
    60  	cf.StringVar(&testMemProfile, "memprofile", "", "")
    61  	cf.String("memprofilerate", "", "")
    62  	cf.StringVar(&testMutexProfile, "mutexprofile", "", "")
    63  	cf.String("mutexprofilefraction", "", "")
    64  	cf.Var(&testOutputDir, "outputdir", "")
    65  	cf.Int("parallel", 0, "")
    66  	cf.String("run", "", "")
    67  	cf.Bool("short", false, "")
    68  	cf.String("skip", "", "")
    69  	cf.DurationVar(&testTimeout, "timeout", 10*time.Minute, "") // known to cmd/dist
    70  	cf.String("fuzztime", "", "")
    71  	cf.String("fuzzminimizetime", "", "")
    72  	cf.StringVar(&testTrace, "trace", "", "")
    73  	cf.Var(&testV, "v", "")
    74  	cf.Var(&testShuffle, "shuffle", "")
    75  
    76  	for name, ok := range passFlagToTest {
    77  		if ok {
    78  			cf.Var(cf.Lookup(name).Value, "test."+name, "")
    79  		}
    80  	}
    81  }
    82  
    83  // outputdirFlag implements the -outputdir flag.
    84  // It interprets an empty value as the working directory of the 'go' command.
    85  type outputdirFlag struct {
    86  	abs string
    87  }
    88  
    89  func (f *outputdirFlag) String() string {
    90  	return f.abs
    91  }
    92  
    93  func (f *outputdirFlag) Set(value string) (err error) {
    94  	if value == "" {
    95  		f.abs = ""
    96  	} else {
    97  		f.abs, err = filepath.Abs(value)
    98  	}
    99  	return err
   100  }
   101  
   102  func (f *outputdirFlag) getAbs() string {
   103  	if f.abs == "" {
   104  		return base.Cwd()
   105  	}
   106  	return f.abs
   107  }
   108  
   109  // vetFlag implements the special parsing logic for the -vet flag:
   110  // a comma-separated list, with distinguished values "all" and
   111  // "off", plus a boolean tracking whether it was set explicitly.
   112  //
   113  // "all" is encoded as vetFlag{true, false, nil}, since it will
   114  // pass no flags to the vet binary, and by default, it runs all
   115  // analyzers.
   116  type vetFlag struct {
   117  	explicit bool
   118  	off      bool
   119  	flags    []string // passed to vet when invoked automatically during 'go test'
   120  }
   121  
   122  func (f *vetFlag) String() string {
   123  	switch {
   124  	case !f.off && !f.explicit && len(f.flags) == 0:
   125  		return "all"
   126  	case f.off:
   127  		return "off"
   128  	}
   129  
   130  	var buf strings.Builder
   131  	for i, f := range f.flags {
   132  		if i > 0 {
   133  			buf.WriteByte(',')
   134  		}
   135  		buf.WriteString(f)
   136  	}
   137  	return buf.String()
   138  }
   139  
   140  func (f *vetFlag) Set(value string) error {
   141  	switch {
   142  	case value == "":
   143  		*f = vetFlag{flags: defaultVetFlags}
   144  		return nil
   145  	case strings.Contains(value, "="):
   146  		return fmt.Errorf("-vet argument cannot contain equal signs")
   147  	case strings.Contains(value, " "):
   148  		return fmt.Errorf("-vet argument is comma-separated list, cannot contain spaces")
   149  	}
   150  
   151  	*f = vetFlag{explicit: true}
   152  	var single string
   153  	for arg := range strings.SplitSeq(value, ",") {
   154  		switch arg {
   155  		case "":
   156  			return fmt.Errorf("-vet argument contains empty list element")
   157  		case "all":
   158  			single = arg
   159  			*f = vetFlag{explicit: true}
   160  			continue
   161  		case "off":
   162  			single = arg
   163  			*f = vetFlag{
   164  				explicit: true,
   165  				off:      true,
   166  			}
   167  			continue
   168  		default:
   169  			if _, ok := passAnalyzersToVet[arg]; !ok {
   170  				return fmt.Errorf("-vet argument must be a supported analyzer or a distinguished value; found %s", arg)
   171  			}
   172  			f.flags = append(f.flags, "-"+arg)
   173  		}
   174  	}
   175  	if len(f.flags) > 1 && single != "" {
   176  		return fmt.Errorf("-vet does not accept %q in a list with other analyzers", single)
   177  	}
   178  	if len(f.flags) > 1 && single != "" {
   179  		return fmt.Errorf("-vet does not accept %q in a list with other analyzers", single)
   180  	}
   181  	return nil
   182  }
   183  
   184  type shuffleFlag struct {
   185  	on   bool
   186  	seed *int64
   187  }
   188  
   189  func (f *shuffleFlag) String() string {
   190  	if !f.on {
   191  		return "off"
   192  	}
   193  	if f.seed == nil {
   194  		return "on"
   195  	}
   196  	return fmt.Sprintf("%d", *f.seed)
   197  }
   198  
   199  func (f *shuffleFlag) Set(value string) error {
   200  	if value == "off" {
   201  		*f = shuffleFlag{on: false}
   202  		return nil
   203  	}
   204  
   205  	if value == "on" {
   206  		*f = shuffleFlag{on: true}
   207  		return nil
   208  	}
   209  
   210  	seed, err := strconv.ParseInt(value, 10, 64)
   211  	if err != nil {
   212  		return fmt.Errorf(`-shuffle argument must be "on", "off", or an int64: %v`, err)
   213  	}
   214  
   215  	*f = shuffleFlag{on: true, seed: &seed}
   216  	return nil
   217  }
   218  
   219  // testFlags processes the command line, grabbing -x and -c, rewriting known flags
   220  // to have "test" before them, and reading the command line for the test binary.
   221  // Unfortunately for us, we need to do our own flag processing because go test
   222  // grabs some flags but otherwise its command line is just a holding place for
   223  // pkg.test's arguments.
   224  // We allow known flags both before and after the package name list,
   225  // to allow both
   226  //
   227  //	go test fmt -custom-flag-for-fmt-test
   228  //	go test -x math
   229  func testFlags(args []string) (packageNames, passToTest []string) {
   230  	base.SetFromGOFLAGS(&CmdTest.Flag)
   231  	addFromGOFLAGS := map[string]bool{}
   232  	CmdTest.Flag.Visit(func(f *flag.Flag) {
   233  		if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
   234  			addFromGOFLAGS[f.Name] = true
   235  		}
   236  	})
   237  
   238  	// firstUnknownFlag helps us report an error when flags not known to 'go
   239  	// test' are used along with -i or -c.
   240  	firstUnknownFlag := ""
   241  
   242  	explicitArgs := make([]string, 0, len(args))
   243  	inPkgList := false
   244  	afterFlagWithoutValue := false
   245  	for len(args) > 0 {
   246  		f, remainingArgs, err := cmdflag.ParseOne(&CmdTest.Flag, args)
   247  
   248  		wasAfterFlagWithoutValue := afterFlagWithoutValue
   249  		afterFlagWithoutValue = false // provisionally
   250  
   251  		if errors.Is(err, flag.ErrHelp) {
   252  			exitWithUsage()
   253  		}
   254  
   255  		if errors.Is(err, cmdflag.ErrFlagTerminator) {
   256  			// 'go list' allows package arguments to be named either before or after
   257  			// the terminator, but 'go test' has historically allowed them only
   258  			// before. Preserve that behavior and treat all remaining arguments —
   259  			// including the terminator itself! — as arguments to the test.
   260  			explicitArgs = append(explicitArgs, args...)
   261  			break
   262  		}
   263  
   264  		if nf, ok := errors.AsType[cmdflag.NonFlagError](err); ok {
   265  			if !inPkgList && packageNames != nil {
   266  				// We already saw the package list previously, and this argument is not
   267  				// a flag, so it — and everything after it — must be either a value for
   268  				// a preceding flag or a literal argument to the test binary.
   269  				if wasAfterFlagWithoutValue {
   270  					// This argument could syntactically be a flag value, so
   271  					// optimistically assume that it is and keep looking for go command
   272  					// flags after it.
   273  					//
   274  					// (If we're wrong, we'll at least be consistent with historical
   275  					// behavior; see https://golang.org/issue/40763.)
   276  					explicitArgs = append(explicitArgs, nf.RawArg)
   277  					args = remainingArgs
   278  					continue
   279  				} else {
   280  					// This argument syntactically cannot be a flag value, so it must be a
   281  					// positional argument, and so must everything after it.
   282  					explicitArgs = append(explicitArgs, args...)
   283  					break
   284  				}
   285  			}
   286  
   287  			inPkgList = true
   288  			packageNames = append(packageNames, nf.RawArg)
   289  			args = remainingArgs // Consume the package name.
   290  			continue
   291  		}
   292  
   293  		if inPkgList {
   294  			// This argument is syntactically a flag, so if we were in the package
   295  			// list we're not anymore.
   296  			inPkgList = false
   297  		}
   298  
   299  		if nd, ok := errors.AsType[cmdflag.FlagNotDefinedError](err); ok {
   300  			// This is a flag we do not know. We must assume that any args we see
   301  			// after this might be flag arguments, not package names, so make
   302  			// packageNames non-nil to indicate that the package list is complete.
   303  			//
   304  			// (Actually, we only strictly need to assume that if the flag is not of
   305  			// the form -x=value, but making this more precise would be a breaking
   306  			// change in the command line API.)
   307  			if packageNames == nil {
   308  				packageNames = []string{}
   309  			}
   310  
   311  			if nd.RawArg == "-args" || nd.RawArg == "--args" {
   312  				// -args or --args signals that everything that follows
   313  				// should be passed to the test.
   314  				explicitArgs = append(explicitArgs, remainingArgs...)
   315  				break
   316  			}
   317  
   318  			if firstUnknownFlag == "" {
   319  				firstUnknownFlag = nd.RawArg
   320  			}
   321  
   322  			explicitArgs = append(explicitArgs, nd.RawArg)
   323  			args = remainingArgs
   324  			if !nd.HasValue {
   325  				afterFlagWithoutValue = true
   326  			}
   327  			continue
   328  		}
   329  
   330  		if err != nil {
   331  			fmt.Fprintln(os.Stderr, err)
   332  			exitWithUsage()
   333  		}
   334  
   335  		if short := strings.TrimPrefix(f.Name, "test."); passFlagToTest[short] {
   336  			explicitArgs = append(explicitArgs, fmt.Sprintf("-test.%s=%v", short, f.Value))
   337  
   338  			// This flag has been overridden explicitly, so don't forward its implicit
   339  			// value from GOFLAGS.
   340  			delete(addFromGOFLAGS, short)
   341  			delete(addFromGOFLAGS, "test."+short)
   342  		}
   343  
   344  		args = remainingArgs
   345  	}
   346  	if firstUnknownFlag != "" && testC {
   347  		fmt.Fprintf(os.Stderr, "go: unknown flag %s cannot be used with -c\n", firstUnknownFlag)
   348  		exitWithUsage()
   349  	}
   350  
   351  	var injectedFlags []string
   352  	if testJSON {
   353  		// If converting to JSON, we need the full output in order to pipe it to test2json.
   354  		// The -test.v=test2json flag is like -test.v=true but causes the test to add
   355  		// extra ^V characters before testing output lines and other framing,
   356  		// which helps test2json do a better job creating the JSON events.
   357  		injectedFlags = append(injectedFlags, "-test.v=test2json")
   358  		delete(addFromGOFLAGS, "v")
   359  		delete(addFromGOFLAGS, "test.v")
   360  
   361  		if gotestjsonbuildtext.Value() == "1" {
   362  			gotestjsonbuildtext.IncNonDefault()
   363  		} else {
   364  			cfg.BuildJSON = true
   365  		}
   366  	}
   367  
   368  	// Inject flags from GOFLAGS before the explicit command-line arguments.
   369  	// (They must appear before the flag terminator or first non-flag argument.)
   370  	// Also determine whether flags with awkward defaults have already been set.
   371  	var timeoutSet, outputDirSet bool
   372  	CmdTest.Flag.Visit(func(f *flag.Flag) {
   373  		short := strings.TrimPrefix(f.Name, "test.")
   374  		if addFromGOFLAGS[f.Name] {
   375  			injectedFlags = append(injectedFlags, fmt.Sprintf("-test.%s=%v", short, f.Value))
   376  		}
   377  		switch short {
   378  		case "timeout":
   379  			timeoutSet = true
   380  		case "outputdir":
   381  			outputDirSet = true
   382  		}
   383  	})
   384  
   385  	// 'go test' has a default timeout, but the test binary itself does not.
   386  	// If the timeout wasn't set (and forwarded) explicitly, add the default
   387  	// timeout to the command line.
   388  	if testTimeout > 0 && !timeoutSet {
   389  		injectedFlags = append(injectedFlags, fmt.Sprintf("-test.timeout=%v", testTimeout))
   390  	}
   391  
   392  	// Similarly, the test binary defaults -test.outputdir to its own working
   393  	// directory, but 'go test' defaults it to the working directory of the 'go'
   394  	// command. Set it explicitly if it is needed due to some other flag that
   395  	// requests output.
   396  	needOutputDir := testProfile() != "" || testArtifacts
   397  	if needOutputDir && !outputDirSet {
   398  		injectedFlags = append(injectedFlags, "-test.outputdir="+testOutputDir.getAbs())
   399  	}
   400  
   401  	// If the user is explicitly passing -help or -h, show output
   402  	// of the test binary so that the help output is displayed
   403  	// even though the test will exit with success.
   404  	// This loop is imperfect: it will do the wrong thing for a case
   405  	// like -args -test.outputdir -help. Such cases are probably rare,
   406  	// and getting this wrong doesn't do too much harm.
   407  helpLoop:
   408  	for _, arg := range explicitArgs {
   409  		switch arg {
   410  		case "--":
   411  			break helpLoop
   412  		case "-h", "-help", "--help":
   413  			testHelp = true
   414  			break helpLoop
   415  		}
   416  	}
   417  
   418  	// Forward any unparsed arguments (following --args) to the test binary.
   419  	return packageNames, append(injectedFlags, explicitArgs...)
   420  }
   421  
   422  func exitWithUsage() {
   423  	fmt.Fprintf(os.Stderr, "usage: %s\n", CmdTest.UsageLine)
   424  	fmt.Fprintf(os.Stderr, "Run 'go help %s' and 'go help %s' for details.\n", CmdTest.LongName(), HelpTestflag.LongName())
   425  
   426  	base.SetExitStatus(2)
   427  	base.Exit()
   428  }
   429  

View as plain text