diff options
Diffstat (limited to 'src/cmd/gotest')
-rw-r--r-- | src/cmd/gotest/Makefile | 12 | ||||
-rw-r--r-- | src/cmd/gotest/doc.go | 113 | ||||
-rw-r--r-- | src/cmd/gotest/flag.go | 159 | ||||
-rw-r--r-- | src/cmd/gotest/gotest.go | 435 |
4 files changed, 719 insertions, 0 deletions
diff --git a/src/cmd/gotest/Makefile b/src/cmd/gotest/Makefile new file mode 100644 index 000000000..5c1154537 --- /dev/null +++ b/src/cmd/gotest/Makefile @@ -0,0 +1,12 @@ +# Copyright 2010 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. + +include ../../Make.inc + +TARG=gotest +GOFILES=\ + flag.go\ + gotest.go\ + +include ../../Make.cmd diff --git a/src/cmd/gotest/doc.go b/src/cmd/gotest/doc.go new file mode 100644 index 000000000..5be06f817 --- /dev/null +++ b/src/cmd/gotest/doc.go @@ -0,0 +1,113 @@ +// Copyright 2009 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. + +/* + +Gotest is an automated testing tool for Go packages. + +Normally a Go package is compiled without its test files. Gotest is a +tool that recompiles the package whose source is in the current +directory, along with any files whose names match the pattern +"[^.]*_test.go". Functions in the test source named TestXXX (where +XXX is any alphanumeric string not starting with a lower case letter) +will be run when the binary is executed. Gotest requires that the +package have a standard package Makefile, one that includes +go/src/Make.pkg. + +The test functions are run in the order they appear in the source. +They should have the signature, + + func TestXXX(t *testing.T) { ... } + +Benchmark functions can be written as well; they will be run only when +the -test.bench flag is provided. Benchmarks should have the +signature, + + func BenchmarkXXX(b *testing.B) { ... } + +See the documentation of the testing package for more information. + +By default, gotest needs no arguments. It compiles all the .go files +in the directory, including tests, and runs the tests. If file names +are given (with flag -file=test.go, one per extra test source file), +only those test files are added to the package. (The non-test files +are always compiled.) + +The package is built in a special subdirectory so it does not +interfere with the non-test installation. + +Usage: + gotest [-file a.go -file b.go ...] [-c] [-x] [args for test binary] + +The flags specific to gotest are: + -c Compile the test binary but do not run it. + -file a.go Use only the tests in the source file a.go. + Multiple -file flags may be provided. + -x Print each subcommand gotest executes. + +Everything else on the command line is passed to the test binary. + +The resulting test binary, called (for amd64) 6.out, has several flags. + +Usage: + 6.out [-test.v] [-test.run pattern] [-test.bench pattern] \ + [-test.cpuprofile=cpu.out] \ + [-test.memprofile=mem.out] [-test.memprofilerate=1] \ + [-test.timeout=10] [-test.short] \ + [-test.benchtime=3] [-test.cpu=1,2,3,4] + +The -test.v flag causes the tests to be logged as they run. The +-test.run flag causes only those tests whose names match the regular +expression pattern to be run. By default all tests are run silently. + +If all specified tests pass, 6.out prints the word PASS and exits with +a 0 exit code. If any tests fail, it prints error details, the word +FAIL, and exits with a non-zero code. The -test.bench flag is +analogous to the -test.run flag, but applies to benchmarks. No +benchmarks run by default. + +The -test.cpuprofile flag causes the testing software to write a CPU +profile to the specified file before exiting. + +The -test.memprofile flag causes the testing software to write a +memory profile to the specified file when all tests are complete. The +-test.memprofilerate flag enables more precise (and expensive) +profiles by setting runtime.MemProfileRate; run + godoc runtime MemProfileRate +for details. The defaults are no memory profile and the standard +setting of MemProfileRate. The memory profile records a sampling of +the memory in use at the end of the test. To profile all memory +allocations, use -test.memprofilerate=1 to sample every byte and set +the environment variable GOGC=off to disable the garbage collector, +provided the test can run in the available memory without garbage +collection. + +Use -test.run or -test.bench to limit profiling to a particular test +or benchmark. + +The -test.short flag tells long-running tests to shorten their run +time. It is off by default but set by all.bash so installations of +the Go tree can do a sanity check but not spend time running +exhaustive tests. + +The -test.timeout flag sets a timeout for the test in seconds. If the +test runs for longer than that, it will panic, dumping a stack trace +of all existing goroutines. + +The -test.benchtime flag specifies the number of seconds to run each benchmark. +The default is one second. + +The -test.cpu flag specifies a list of GOMAXPROCS values for which +the tests or benchmarks are executed. The default is the current +value of GOMAXPROCS. + +For convenience, each of these -test.X flags of the test binary is +also available as the flag -X in gotest itself. Flags not listed here +are unaffected. For instance, the command + gotest -x -v -cpuprofile=prof.out -dir=testdata -update -file x_test.go +will compile the test binary using x_test.go and then run it as + 6.out -test.v -test.cpuprofile=prof.out -dir=testdata -update + +*/ +package documentation diff --git a/src/cmd/gotest/flag.go b/src/cmd/gotest/flag.go new file mode 100644 index 000000000..c3a28f9a3 --- /dev/null +++ b/src/cmd/gotest/flag.go @@ -0,0 +1,159 @@ +// 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 gotest 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 %s: + -c=false: compile but do not run the test binary + -file=file: + -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 + -benchtime=1: passes -test.benchtime to test + -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 + -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 usage() { + fmt.Fprintf(os.Stdout, usageMessage, os.Args[0]) + os.Exit(2) +} + +// flagSpec defines a flag we know about. +type flagSpec struct { + name string + isBool bool + passToTest bool // pass to Test + multiOK bool // OK to have multiple instances + present bool // flag has been seen +} + +// flagDefn is the set of flags we process. +var flagDefn = []*flagSpec{ + // gotest-local. + &flagSpec{name: "c", isBool: true}, + &flagSpec{name: "file", multiOK: true}, + &flagSpec{name: "x", isBool: true}, + + // passed to 6.out, adding a "test." prefix to the name if necessary: -v becomes -test.v. + &flagSpec{name: "bench", passToTest: true}, + &flagSpec{name: "benchtime", passToTest: true}, + &flagSpec{name: "cpu", passToTest: true}, + &flagSpec{name: "cpuprofile", passToTest: true}, + &flagSpec{name: "memprofile", passToTest: true}, + &flagSpec{name: "memprofilerate", passToTest: true}, + &flagSpec{name: "run", passToTest: true}, + &flagSpec{name: "short", isBool: true, passToTest: true}, + &flagSpec{name: "timeout", passToTest: true}, + &flagSpec{name: "v", isBool: true, passToTest: true}, +} + +// flags 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 gotest +// grabs some flags but otherwise its command line is just a holding place for +// 6.out's arguments. +func flags() { + for i := 1; i < len(os.Args); i++ { + arg := os.Args[i] + f, value, extraWord := flag(i) + if f == nil { + args = append(args, arg) + continue + } + switch f.name { + case "c": + setBoolFlag(&cFlag, value) + case "x": + setBoolFlag(&xFlag, value) + case "file": + fileNames = append(fileNames, value) + } + if extraWord { + i++ + } + if f.passToTest { + args = append(args, "-test."+f.name+"="+value) + } + } +} + +// flag sees if argument i is a known flag and returns its definition, value, and whether it consumed an extra word. +func flag(i int) (f *flagSpec, value string, extra bool) { + arg := os.Args[i] + if strings.HasPrefix(arg, "--") { // reduce two minuses to one + arg = arg[1:] + } + if arg == "" || arg[0] != '-' { + return + } + name := arg[1:] + // If there's already "test.", drop it for now. + if strings.HasPrefix(name, "test.") { + name = name[5:] + } + equals := strings.Index(name, "=") + if equals >= 0 { + value = name[equals+1:] + name = name[:equals] + } + for _, f = range flagDefn { + if name == f.name { + // Booleans are special because they have modes -x, -x=true, -x=false. + if f.isBool { + 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(os.Args) { + usage() + } + value = os.Args[i+1] + } + } + if f.present && !f.multiOK { + usage() + } + f.present = true + return + } + } + f = nil + return +} + +// setBoolFlag sets the addressed boolean to the value. +func setBoolFlag(flag *bool, value string) { + x, err := strconv.Atob(value) + if err != nil { + fmt.Fprintf(os.Stderr, "gotest: illegal bool flag value %s\n", value) + usage() + } + *flag = x +} diff --git a/src/cmd/gotest/gotest.go b/src/cmd/gotest/gotest.go new file mode 100644 index 000000000..4cb3da23c --- /dev/null +++ b/src/cmd/gotest/gotest.go @@ -0,0 +1,435 @@ +// 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 ( + "bufio" + "exec" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "time" + "unicode" + "utf8" +) + +// Environment for commands. +var ( + XGC []string // 6g -I _test -o _xtest_.6 + GC []string // 6g -I _test _testmain.go + GL []string // 6l -L _test _testmain.6 + GOARCH string + GOROOT string + GORUN string + O string + args []string // arguments passed to gotest; also passed to the binary + fileNames []string + env = os.Environ() +) + +// These strings are created by getTestNames. +var ( + insideFileNames []string // list of *.go files inside the package. + outsideFileNames []string // list of *.go files outside the package (in package foo_test). +) + +var ( + files []*File + importPath string +) + +// Flags for our own purposes. We do our own flag processing. +var ( + cFlag bool + xFlag bool +) + +// elapsed returns the number of seconds since gotest started. +func elapsed() float64 { + return float64(time.Nanoseconds()-start) / 1e9 +} + +var start = time.Nanoseconds() + +// File represents a file that contains tests. +type File struct { + name string + pkg string + file *os.File + astFile *ast.File + tests []string // The names of the TestXXXs. + benchmarks []string // The names of the BenchmarkXXXs. +} + +func main() { + flags() + needMakefile() + setEnvironment() + getTestFileNames() + parseFiles() + getTestNames() + run("gomake", "testpackage-clean") + run("gomake", "testpackage", fmt.Sprintf("GOTESTFILES=%s", strings.Join(insideFileNames, " "))) + if len(outsideFileNames) > 0 { + run(append(XGC, outsideFileNames...)...) + } + importPath = runWithStdout("gomake", "-s", "importpath") + writeTestmainGo() + run(GC...) + run(GL...) + if !cFlag { + runTestWithArgs("./" + O + ".out") + } + if xFlag { + fmt.Printf("gotest %.2fs: done\n", elapsed()) + } +} + +// needMakefile tests that we have a Makefile in this directory. +func needMakefile() { + if _, err := os.Stat("Makefile"); err != nil { + Fatalf("please create a Makefile for gotest; see http://golang.org/doc/code.html for details") + } +} + +// Fatalf formats its arguments, prints the message with a final newline, and exits. +func Fatalf(s string, args ...interface{}) { + fmt.Fprintf(os.Stderr, "gotest: "+s+"\n", args...) + os.Exit(2) +} + +// theChar is the map from architecture to object character. +var theChar = map[string]string{ + "arm": "5", + "amd64": "6", + "386": "8", +} + +// addEnv adds a name=value pair to the environment passed to subcommands. +// If the item is already in the environment, addEnv replaces the value. +func addEnv(name, value string) { + for i := 0; i < len(env); i++ { + if strings.HasPrefix(env[i], name+"=") { + env[i] = name + "=" + value + return + } + } + env = append(env, name+"="+value) +} + +// setEnvironment assembles the configuration for gotest and its subcommands. +func setEnvironment() { + // Basic environment. + GOROOT = runtime.GOROOT() + addEnv("GOROOT", GOROOT) + GOARCH = os.Getenv("GOARCH") + if GOARCH == "" { + GOARCH = runtime.GOARCH + } + addEnv("GOARCH", GOARCH) + O = theChar[GOARCH] + if O == "" { + Fatalf("unknown architecture %s", GOARCH) + } + + // Commands and their flags. + gc := os.Getenv("GC") + if gc == "" { + gc = O + "g" + } + XGC = []string{gc, "-I", "_test", "-o", "_xtest_." + O} + GC = []string{gc, "-I", "_test", "_testmain.go"} + gl := os.Getenv("GL") + if gl == "" { + gl = O + "l" + } + GL = []string{gl, "-L", "_test", "_testmain." + O} + + // Silence make on Linux + addEnv("MAKEFLAGS", "") + addEnv("MAKELEVEL", "") +} + +// getTestFileNames gets the set of files we're looking at. +// If gotest has no arguments, it scans for file names matching "[^.]*_test.go". +func getTestFileNames() { + names := fileNames + if len(names) == 0 { + var err os.Error + names, err = filepath.Glob("[^.]*_test.go") + if err != nil { + Fatalf("Glob pattern error: %s", err) + } + if len(names) == 0 { + Fatalf(`no test files found: no match for "[^.]*_test.go"`) + } + } + for _, n := range names { + fd, err := os.Open(n) + if err != nil { + Fatalf("%s: %s", n, err) + } + f := &File{name: n, file: fd} + files = append(files, f) + } +} + +// parseFiles parses the files and remembers the packages we find. +func parseFiles() { + fileSet := token.NewFileSet() + for _, f := range files { + // Report declaration errors so we can abort if the files are incorrect Go. + file, err := parser.ParseFile(fileSet, f.name, nil, parser.DeclarationErrors) + if err != nil { + Fatalf("parse error: %s", err) + } + f.astFile = file + f.pkg = file.Name.String() + if f.pkg == "" { + Fatalf("cannot happen: no package name in %s", f.name) + } + } +} + +// getTestNames extracts the names of tests and benchmarks. They are all +// top-level functions that are not methods. +func getTestNames() { + for _, f := range files { + for _, d := range f.astFile.Decls { + n, ok := d.(*ast.FuncDecl) + if !ok { + continue + } + if n.Recv != nil { // a method, not a function. + continue + } + name := n.Name.String() + if isTest(name, "Test") { + f.tests = append(f.tests, name) + } else if isTest(name, "Benchmark") { + f.benchmarks = append(f.benchmarks, name) + } + // TODO: worth checking the signature? Probably not. + } + if strings.HasSuffix(f.pkg, "_test") { + outsideFileNames = append(outsideFileNames, f.name) + } else { + insideFileNames = append(insideFileNames, f.name) + } + } +} + +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(rune) +} + +func run(args ...string) { + doRun(args, false) +} + +// runWithStdout is like run, but returns the text of standard output with the last newline dropped. +func runWithStdout(argv ...string) string { + s := doRun(argv, true) + if strings.HasSuffix(s, "\r\n") { + s = s[:len(s)-2] + } else if strings.HasSuffix(s, "\n") { + s = s[:len(s)-1] + } + if len(s) == 0 { + Fatalf("no output from command %s", strings.Join(argv, " ")) + } + return s +} + +// runTestWithArgs appends gotest's runs the provided binary with the args passed on the command line. +func runTestWithArgs(binary string) { + doRun(append([]string{binary}, args...), false) +} + +// doRun is the general command runner. The flag says whether we want to +// retrieve standard output. +func doRun(argv []string, returnStdout bool) string { + if xFlag { + fmt.Printf("gotest %.2fs: %s\n", elapsed(), strings.Join(argv, " ")) + t := -time.Nanoseconds() + defer func() { + t += time.Nanoseconds() + fmt.Printf(" [+%.2fs]\n", float64(t)/1e9) + }() + } + command := argv[0] + if runtime.GOOS == "windows" && command == "gomake" { + // gomake is a shell script and it cannot be executed directly on Windows. + cmd := "" + for i, v := range argv { + if i > 0 { + cmd += " " + } + cmd += `"` + v + `"` + } + command = "bash" + argv = []string{"bash", "-c", cmd} + } + var err os.Error + argv[0], err = exec.LookPath(argv[0]) + if err != nil { + Fatalf("can't find %s: %s", command, err) + } + procAttr := &os.ProcAttr{ + Env: env, + Files: []*os.File{ + os.Stdin, + os.Stdout, + os.Stderr, + }, + } + var r, w *os.File + if returnStdout { + r, w, err = os.Pipe() + if err != nil { + Fatalf("can't create pipe: %s", err) + } + procAttr.Files[1] = w + } + proc, err := os.StartProcess(argv[0], argv, procAttr) + if err != nil { + Fatalf("%s failed to start: %s", command, err) + } + if returnStdout { + defer r.Close() + w.Close() + } + waitMsg, err := proc.Wait(0) + if err != nil || waitMsg == nil { + Fatalf("%s failed: %s", command, err) + } + if !waitMsg.Exited() || waitMsg.ExitStatus() != 0 { + Fatalf("%q failed: %s", strings.Join(argv, " "), waitMsg) + } + if returnStdout { + b, err := ioutil.ReadAll(r) + if err != nil { + Fatalf("can't read output from command: %s", err) + } + return string(b) + } + return "" +} + +// writeTestmainGo generates the test program to be compiled, "./_testmain.go". +func writeTestmainGo() { + f, err := os.Create("_testmain.go") + if err != nil { + Fatalf("can't create _testmain.go: %s", err) + } + defer f.Close() + b := bufio.NewWriter(f) + defer b.Flush() + + // Package and imports. + fmt.Fprint(b, "package main\n\n") + // Are there tests from a package other than the one we're testing? + // We can't just use file names because some of the things we compiled + // contain no tests. + outsideTests := false + insideTests := false + for _, f := range files { + //println(f.name, f.pkg) + if len(f.tests) == 0 && len(f.benchmarks) == 0 { + continue + } + if strings.HasSuffix(f.pkg, "_test") { + outsideTests = true + } else { + insideTests = true + } + } + if insideTests { + switch importPath { + case "testing": + case "main": + // Import path main is reserved, so import with + // explicit reference to ./_test/main instead. + // Also, the file we are writing defines a function named main, + // so rename this import to __main__ to avoid name conflict. + fmt.Fprintf(b, "import __main__ %q\n", "./_test/main") + default: + fmt.Fprintf(b, "import %q\n", importPath) + } + } + if outsideTests { + fmt.Fprintf(b, "import %q\n", "./_xtest_") + } + fmt.Fprintf(b, "import %q\n", "testing") + fmt.Fprintf(b, "import __os__ %q\n", "os") // rename in case tested package is called os + fmt.Fprintf(b, "import __regexp__ %q\n", "regexp") // rename in case tested package is called regexp + fmt.Fprintln(b) // for gofmt + + // Tests. + fmt.Fprintln(b, "var tests = []testing.InternalTest{") + for _, f := range files { + for _, t := range f.tests { + fmt.Fprintf(b, "\t{\"%s.%s\", %s.%s},\n", f.pkg, t, notMain(f.pkg), t) + } + } + fmt.Fprintln(b, "}") + fmt.Fprintln(b) + + // Benchmarks. + fmt.Fprintf(b, "var benchmarks = []testing.InternalBenchmark{") + for _, f := range files { + for _, bm := range f.benchmarks { + fmt.Fprintf(b, "\t{\"%s.%s\", %s.%s},\n", f.pkg, bm, notMain(f.pkg), bm) + } + } + fmt.Fprintln(b, "}") + + // Body. + fmt.Fprintln(b, testBody) +} + +// notMain returns the package, renaming as appropriate if it's "main". +func notMain(pkg string) string { + if pkg == "main" { + return "__main__" + } + return pkg +} + +// testBody is just copied to the output. It's the code that runs the tests. +var testBody = ` +var matchPat string +var matchRe *__regexp__.Regexp + +func matchString(pat, str string) (result bool, err __os__.Error) { + if matchRe == nil || matchPat != pat { + matchPat = pat + matchRe, err = __regexp__.Compile(matchPat) + if err != nil { + return + } + } + return matchRe.MatchString(str), nil +} + +func main() { + testing.Main(matchString, tests, benchmarks) +}` |