// 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 main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

// The flag handling part of go test is large and distracting.
// We can't use the flag package because some of the flags from
// our command line are for us, and some are for 6.out, and
// some are for both.

var usageMessage = `Usage of go test:
  -c=false: compile but do not run the test binary
  -file=file_test.go: specify file to use for tests;
      use multiple times for multiple files
  -p=n: build and test up to n packages in parallel
  -x=false: print command lines as they are executed

  // These flags can be passed with or without a "test." prefix: -v or -test.v.
  -bench="": passes -test.bench to test
  -benchmem=false: print memory allocation statistics for benchmarks
  -benchtime=1s: passes -test.benchtime to test
  -cover=false: enable coverage analysis
  -covermode="set": specifies mode for coverage analysis
  -coverpkg="": comma-separated list of packages for coverage analysis
  -coverprofile="": passes -test.coverprofile to test if -cover
  -cpu="": passes -test.cpu to test
  -cpuprofile="": passes -test.cpuprofile to test
  -memprofile="": passes -test.memprofile to test
  -memprofilerate=0: passes -test.memprofilerate to test
  -blockprofile="": pases -test.blockprofile to test
  -blockprofilerate=0: passes -test.blockprofilerate to test
  -outputdir=$PWD: passes -test.outputdir to test
  -parallel=0: passes -test.parallel to test
  -run="": passes -test.run to test
  -short=false: passes -test.short to test
  -timeout=0: passes -test.timeout to test
  -v=false: passes -test.v to test
`

// usage prints a usage message and exits.
func testUsage() {
	fmt.Fprint(os.Stderr, usageMessage)
	setExitStatus(2)
	exit()
}

// testFlagSpec defines a flag we know about.
type testFlagSpec struct {
	name       string
	boolVar    *bool
	passToTest bool // pass to Test
	multiOK    bool // OK to have multiple instances
	present    bool // flag has been seen
}

// testFlagDefn is the set of flags we process.
var testFlagDefn = []*testFlagSpec{
	// local.
	{name: "c", boolVar: &testC},
	{name: "cover", boolVar: &testCover},
	{name: "coverpkg"},
	{name: "o"},

	// build flags.
	{name: "a", boolVar: &buildA},
	{name: "n", boolVar: &buildN},
	{name: "p"},
	{name: "x", boolVar: &buildX},
	{name: "i", boolVar: &buildI},
	{name: "work", boolVar: &buildWork},
	{name: "ccflags"},
	{name: "gcflags"},
	{name: "exec"},
	{name: "ldflags"},
	{name: "gccgoflags"},
	{name: "tags"},
	{name: "compiler"},
	{name: "race", boolVar: &buildRace},
	{name: "installsuffix"},

	// passed to 6.out, adding a "test." prefix to the name if necessary: -v becomes -test.v.
	{name: "bench", passToTest: true},
	{name: "benchmem", boolVar: new(bool), passToTest: true},
	{name: "benchtime", passToTest: true},
	{name: "covermode"},
	{name: "coverprofile", passToTest: true},
	{name: "cpu", passToTest: true},
	{name: "cpuprofile", passToTest: true},
	{name: "memprofile", passToTest: true},
	{name: "memprofilerate", passToTest: true},
	{name: "blockprofile", passToTest: true},
	{name: "blockprofilerate", passToTest: true},
	{name: "outputdir", passToTest: true},
	{name: "parallel", passToTest: true},
	{name: "run", passToTest: true},
	{name: "short", boolVar: new(bool), passToTest: true},
	{name: "timeout", passToTest: true},
	{name: "v", boolVar: &testV, passToTest: true},
}

// testFlags processes the command line, grabbing -x and -c, rewriting known flags
// to have "test" before them, and reading the command line for the 6.out.
// 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) {
	inPkg := false
	outputDir := ""
	for i := 0; i < len(args); i++ {
		if !strings.HasPrefix(args[i], "-") {
			if !inPkg && packageNames == nil {
				// First package name we've seen.
				inPkg = true
			}
			if inPkg {
				packageNames = append(packageNames, args[i])
				continue
			}
		}

		if inPkg {
			// Found an argument beginning with "-"; end of package list.
			inPkg = false
		}

		f, value, extraWord := testFlag(args, i)
		if f == nil {
			// 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.
			inPkg = false
			if packageNames == nil {
				// make non-nil: we have seen the empty package list
				packageNames = []string{}
			}
			passToTest = append(passToTest, args[i])
			continue
		}
		var err error
		switch f.name {
		// bool flags.
		case "a", "c", "i", "n", "x", "v", "race", "cover", "work":
			setBoolFlag(f.boolVar, value)
		case "o":
			testO = value
			testNeedBinary = true
		case "p":
			setIntFlag(&buildP, value)
		case "exec":
			execCmd, err = splitQuotedFields(value)
			if err != nil {
				fatalf("invalid flag argument for -%s: %v", f.name, err)
			}
		case "ccflags":
			buildCcflags, err = splitQuotedFields(value)
			if err != nil {
				fatalf("invalid flag argument for -%s: %v", f.name, err)
			}
		case "gcflags":
			buildGcflags, err = splitQuotedFields(value)
			if err != nil {
				fatalf("invalid flag argument for -%s: %v", f.name, err)
			}
		case "ldflags":
			buildLdflags, err = splitQuotedFields(value)
			if err != nil {
				fatalf("invalid flag argument for -%s: %v", f.name, err)
			}
		case "gccgoflags":
			buildGccgoflags, err = splitQuotedFields(value)
			if err != nil {
				fatalf("invalid flag argument for -%s: %v", f.name, err)
			}
		case "tags":
			buildContext.BuildTags = strings.Fields(value)
		case "compiler":
			buildCompiler{}.Set(value)
		case "bench":
			// record that we saw the flag; don't care about the value
			testBench = true
		case "timeout":
			testTimeout = value
		case "blockprofile", "cpuprofile", "memprofile":
			testProfile = true
			testNeedBinary = true
		case "coverpkg":
			testCover = true
			if value == "" {
				testCoverPaths = nil
			} else {
				testCoverPaths = strings.Split(value, ",")
			}
		case "coverprofile":
			testCover = true
			testProfile = true
		case "covermode":
			switch value {
			case "set", "count", "atomic":
				testCoverMode = value
			default:
				fatalf("invalid flag argument for -cover: %q", value)
			}
			testCover = true
		case "outputdir":
			outputDir = value
		}
		if extraWord {
			i++
		}
		if f.passToTest {
			passToTest = append(passToTest, "-test."+f.name+"="+value)
		}
	}

	if testCoverMode == "" {
		testCoverMode = "set"
		if buildRace {
			// Default coverage mode is atomic when -race is set.
			testCoverMode = "atomic"
		}
	}

	// Tell the test what directory we're running in, so it can write the profiles there.
	if testProfile && outputDir == "" {
		dir, err := os.Getwd()
		if err != nil {
			fatalf("error from os.Getwd: %s", err)
		}
		passToTest = append(passToTest, "-test.outputdir", dir)
	}
	return
}

// testFlag sees if argument i is a known flag and returns its definition, value, and whether it consumed an extra word.
func testFlag(args []string, i int) (f *testFlagSpec, value string, extra bool) {
	arg := args[i]
	if strings.HasPrefix(arg, "--") { // reduce two minuses to one
		arg = arg[1:]
	}
	switch arg {
	case "-?", "-h", "-help":
		usage()
	}
	if arg == "" || arg[0] != '-' {
		return
	}
	name := arg[1:]
	// If there's already "test.", drop it for now.
	name = strings.TrimPrefix(name, "test.")
	equals := strings.Index(name, "=")
	if equals >= 0 {
		value = name[equals+1:]
		name = name[:equals]
	}
	for _, f = range testFlagDefn {
		if name == f.name {
			// Booleans are special because they have modes -x, -x=true, -x=false.
			if f.boolVar != nil {
				if equals < 0 { // otherwise, it's been set and will be verified in setBoolFlag
					value = "true"
				} else {
					// verify it parses
					setBoolFlag(new(bool), value)
				}
			} else { // Non-booleans must have a value.
				extra = equals < 0
				if extra {
					if i+1 >= len(args) {
						testSyntaxError("missing argument for flag " + f.name)
					}
					value = args[i+1]
				}
			}
			if f.present && !f.multiOK {
				testSyntaxError(f.name + " flag may be set only once")
			}
			f.present = true
			return
		}
	}
	f = nil
	return
}

// setBoolFlag sets the addressed boolean to the value.
func setBoolFlag(flag *bool, value string) {
	x, err := strconv.ParseBool(value)
	if err != nil {
		testSyntaxError("illegal bool flag value " + value)
	}
	*flag = x
}

// setIntFlag sets the addressed integer to the value.
func setIntFlag(flag *int, value string) {
	x, err := strconv.Atoi(value)
	if err != nil {
		testSyntaxError("illegal int flag value " + value)
	}
	*flag = x
}

func testSyntaxError(msg string) {
	fmt.Fprintf(os.Stderr, "go test: %s\n", msg)
	fmt.Fprintf(os.Stderr, `run "go help test" or "go help testflag" for more information`+"\n")
	os.Exit(2)
}
