| // 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/build" |
| "go/doc" |
| "go/parser" |
| "go/token" |
| "io/ioutil" |
| "os" |
| "runtime" |
| "sort" |
| "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. |
| examples []example |
| } |
| |
| type example struct { |
| name string // The name of the example function (ExampleXXX). |
| output string // The expected output (stdout/stderr) of the function. |
| } |
| |
| 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" |
| } |
| var gcflags []string |
| if gf := strings.TrimSpace(os.Getenv("GCFLAGS")); gf != "" { |
| gcflags = strings.Fields(gf) |
| } |
| XGC = append([]string{gc, "-I", "_test", "-o", "_xtest_." + O}, gcflags...) |
| GC = append(append([]string{gc, "-I", "_test"}, gcflags...), "_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 the current directory |
| // for test files. |
| func getTestFileNames() { |
| names := fileNames |
| if len(names) == 0 { |
| info, err := build.ScanDir(".") |
| if err != nil { |
| Fatalf("scanning directory: %v", err) |
| } |
| names = append(info.TestGoFiles, info.XTestGoFiles...) |
| sort.Strings(names) |
| if len(names) == 0 { |
| Fatalf("no test files found in current directory") |
| } |
| } |
| 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|parser.ParseComments) |
| 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) |
| } else if isTest(name, "Example") { |
| f.examples = append(f.examples, example{ |
| name: name, |
| output: doc.CommentText(n.Doc), |
| }) |
| } |
| // TODO: worth checking the signature? Probably not. |
| } |
| if isOutsideTest(f.pkg) { |
| 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 isOutsideTest(f.pkg) { |
| outsideTests = true |
| } else { |
| insideTests = true |
| } |
| } |
| // Rename the imports for the system under test, |
| // in case the tested package has the same name |
| // as any of the other imports, variables or methods. |
| if insideTests { |
| switch importPath { |
| case "testing": |
| case "main": |
| // Import path main is reserved, so import with |
| // explicit reference to ./_test/main instead. |
| fmt.Fprintf(b, "import target %q\n", "./_test/main") |
| default: |
| fmt.Fprintf(b, "import target %q\n", importPath) |
| } |
| } |
| if outsideTests { |
| // It is possible to have both inside and outside tests |
| // at the same time, so a different import name is needed. |
| fmt.Fprintf(b, "import target_test %q\n", "./_xtest_") |
| } |
| fmt.Fprintf(b, "import %q\n", "testing") |
| fmt.Fprintf(b, "import %q\n", "os") |
| fmt.Fprintf(b, "import %q\n", "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, renamedPackage(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, renamedPackage(f.pkg), bm) |
| } |
| } |
| fmt.Fprintln(b, "}") |
| |
| // Examples. |
| fmt.Fprintf(b, "var examples = []testing.InternalExample{") |
| for _, f := range files { |
| for _, eg := range f.examples { |
| fmt.Fprintf(b, "\t{%q, %s.%s, %q},\n", eg.name, renamedPackage(f.pkg), eg.name, eg.output) |
| } |
| } |
| fmt.Fprintln(b, "}") |
| |
| // Body. |
| fmt.Fprintln(b, testBody) |
| } |
| |
| // renamedPackage returns the name under which the test package was imported. |
| func renamedPackage(pkg string) string { |
| if isOutsideTest(pkg) { |
| return "target_test" |
| } |
| return "target" |
| } |
| |
| func isOutsideTest(pkg string) bool { |
| return strings.HasSuffix(pkg, "_test") |
| } |
| |
| // 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, examples) |
| }` |