| // 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) |
| }` |