mirror of
https://github.com/golang/go
synced 2024-11-13 18:30:26 -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:
parent
536531769b
commit
e393efc499
435
src/cmd/gotest/ngotest.go
Normal file
435
src/cmd/gotest/ngotest.go
Normal 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)
|
||||||
|
}`
|
Loading…
Reference in New Issue
Block a user