1
0
mirror of https://github.com/golang/go synced 2024-11-21 22:14:41 -07:00

ngotest: a new gotest command, written in Go.

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
This commit is contained in:
Rob Pike 2011-03-29 10:11:33 -07:00
parent 536531769b
commit e393efc499

435
src/cmd/gotest/ngotest.go Normal file
View File

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