]> Cypherpunks repositories - gostls13.git/commitdiff
ngotest: a new gotest command, written in Go.
authorRob Pike <r@golang.org>
Tue, 29 Mar 2011 17:11:33 +0000 (10:11 -0700)
committerRob Pike <r@golang.org>
Tue, 29 Mar 2011 17:11:33 +0000 (10:11 -0700)
It runs all tests correctly and saves significant time by avoiding the shell script.
However, this is just the code for the command, for review.
A separate CL will move this into the real gotest, which will take some dancing.

R=rsc, peterGo, bsiegert, albert.strasheim, rog, niemeyer, r2
CC=golang-dev
https://golang.org/cl/4281073

src/cmd/gotest/ngotest.go [new file with mode: 0644]

diff --git a/src/cmd/gotest/ngotest.go b/src/cmd/gotest/ngotest.go
new file mode 100644 (file)
index 0000000..6eb2dbe
--- /dev/null
@@ -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"
+       "flag"
+       "fmt"
+       "go/ast"
+       "go/parser"
+       "go/token"
+       "io/ioutil"
+       "os"
+       "path/filepath"
+       "runtime"
+       "strings"
+       "unicode"
+       "utf8"
+)
+
+// Environment for commands.
+var (
+       GC     []string // 6g -I _test _testmain.go
+       GL     []string // 6l -L _test _testmain.6
+       GOARCH string
+       GOROOT string
+       GORUN  string
+       O      string
+       env    = os.Environ()
+)
+
+var (
+       files      = make([]*File, 0, 10)
+       importPath string
+)
+
+// Flags from package "testing" we will forward to 6.out.  See documentation there
+// or by running "godoc gotest" - details are in ./doc.go.
+var (
+       test_short          bool
+       test_v              bool
+       test_run            string
+       test_memprofile     string
+       test_memprofilerate int
+       test_cpuprofile     string
+)
+
+// Flags for our own purposes
+var (
+       xFlag = flag.Bool("x", false, "print command lines as they are executed")
+       cFlag = flag.Bool("c", false, "compile but do not run the test binary")
+)
+
+// 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() {
+       flag.Parse()
+       needMakefile()
+       setEnvironment()
+       getTestFileNames()
+       parseFiles()
+       getTestNames()
+       run("gomake", "testpackage-clean")
+       run("gomake", "testpackage", fmt.Sprintf("GOTESTFILES=%s", insideFileNames()))
+       importPath = runWithStdout("gomake", "-s", "importpath")
+       writeTestmainGo()
+       run(GC...)
+       run(GL...)
+       if !*cFlag {
+               runWithTestFlags("./" + O + ".out")
+       }
+}
+
+// init sets up pairs of flags.  Each pair contains a flag defined as in testing, and one with
+// the "test." prefix missing, for ease of use.
+func init() {
+       flag.BoolVar(&test_short, "test.short", false, "run smaller test suite to save time")
+       flag.BoolVar(&test_v, "test.v", false, "verbose: print additional output")
+       flag.StringVar(&test_run, "test.run", "", "regular expression to select tests to run")
+       flag.StringVar(&test_memprofile, "test.memprofile", "", "write a memory profile to the named file after execution")
+       flag.IntVar(&test_memprofilerate, "test.memprofilerate", 0, "if >=0, sets runtime.MemProfileRate")
+       flag.StringVar(&test_cpuprofile, "test.cpuprofile", "", "write a cpu profile to the named file during execution")
+       // Now the same flags again, but with shorter names that are forwarded.
+       flag.BoolVar(&test_short, "short", false, "passes -test.short to test")
+       flag.BoolVar(&test_v, "v", false, "passes -test.v to test")
+       flag.StringVar(&test_run, "run", "", "passes -test.run to test")
+       flag.StringVar(&test_memprofile, "memprofile", "", "passes -test.memprofile to test")
+       flag.IntVar(&test_memprofilerate, "memprofilerate", 0, "passes -test.memprofilerate to test")
+       flag.StringVar(&test_cpuprofile, "cpuprofile", "", "passes -test.cpuprofile to test")
+}
+
+// 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 = 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"
+       }
+       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 the current directory for _test.go files.
+func getTestFileNames() {
+       names := flag.Args()
+       if len(names) == 0 {
+               names = filepath.Glob("*_test.go")
+               if len(names) == 0 {
+                       Fatalf(`no test files found: no match for "*_test.go"`)
+               }
+       }
+       for _, n := range names {
+               fd, err := os.Open(n, os.O_RDONLY, 0)
+               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 {
+               file, err := parser.ParseFile(fileSet, f.name, nil, 0)
+               if err != nil {
+                       Fatalf("could not parse %s: %s", f.name, 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.
+               }
+       }
+}
+
+// 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) || len(name) == len(prefix) {
+               return false
+       }
+       rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
+       return !unicode.IsLower(rune)
+}
+
+// insideFileNames returns the list of files in package foo, not a package foo_test, as a space-separated string.
+func insideFileNames() (result string) {
+       for _, f := range files {
+               if !strings.HasSuffix(f.pkg, "_test") {
+                       if len(result) > 0 {
+                               result += " "
+                       }
+                       result += f.name
+               }
+       }
+       return
+}
+
+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 len(s) == 0 {
+               Fatalf("no output from command %s", strings.Join(argv, " "))
+       }
+       if s[len(s)-1] == '\n' {
+               s = s[:len(s)-1]
+       }
+       return s
+}
+
+// runWithTestFlags appends any flag settings to the command line before running it.
+func runWithTestFlags(argv ...string) {
+       if test_short {
+               argv = append(argv, "-test.short")
+       }
+
+       if test_v {
+               argv = append(argv, "-test.v")
+       }
+       if test_run != "" {
+               argv = append(argv, fmt.Sprintf("-test.run=%s", test_run))
+       }
+       if test_memprofile != "" {
+               argv = append(argv, fmt.Sprintf("-test.memprofile=%s", test_memprofile))
+       }
+       if test_memprofilerate > 0 {
+               argv = append(argv, fmt.Sprintf("-test.memprofilerate=%d", test_memprofilerate))
+       }
+       if test_cpuprofile != "" {
+               argv = append(argv, fmt.Sprintf("-test.cpuprofile=%s", test_cpuprofile))
+       }
+       doRun(argv, 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: %s\n", strings.Join(argv, " "))
+       }
+       var err os.Error
+       argv[0], err = exec.LookPath(argv[0])
+       if err != nil {
+               Fatalf("can't find %s: %s", argv[0], 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("make failed to start: %s", err)
+       }
+       if returnStdout {
+               defer r.Close()
+               w.Close()
+       }
+       waitMsg, err := proc.Wait(0)
+       if err != nil || waitMsg == nil {
+               Fatalf("%s failed: %s", argv[0], 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.Open("_testmain.go", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)
+       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?
+       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.Fprintln(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)
+}`