Skip to content

Add support for TestMain function in test mode #380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 5, 2016
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 132 additions & 24 deletions tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package main

import (
"bytes"
"errors"
"fmt"
"go/ast"
"go/build"
"go/doc"
"go/parser"
"go/scanner"
"go/token"
Expand All @@ -18,11 +20,14 @@ import (
"path"
"path/filepath"
"runtime"
// "sort"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

(I guess it's commented out because of the example code being commented out, right? In any case, it's unclear when examples will work, so this may stay here for a while, and it's not helpful. Better to remove it now and add it when it's needed.)

"strconv"
"strings"
"syscall"
"text/template"
"time"
"unicode"
"unicode/utf8"

gbuild "github.com/gopherjs/gopherjs/build"
"github.com/gopherjs/gopherjs/compiler"
Expand Down Expand Up @@ -325,25 +330,27 @@ func main() {
fmt.Printf("? \t%s\t[no test files]\n", pkg.ImportPath)
continue
}

s := gbuild.NewSession(options)

tests := &testFuncs{Package: pkg.Package}
collectTests := func(testPkg *gbuild.PackageData, testPkgName string, needVar *bool) error {
archive, err := s.BuildPackage(testPkg)
if err != nil {
return err
}

for _, decl := range archive.Declarations {
if strings.HasPrefix(decl.FullName, testPkg.ImportPath+".Test") {
tests.Tests = append(tests.Tests, testFunc{Package: testPkgName, Name: decl.FullName[len(testPkg.ImportPath)+1:]})
*needVar = true
if testPkgName == "_test" {
for _, file := range pkg.TestGoFiles {
if err := tests.load(filepath.Join(pkg.Package.Dir, file), testPkgName, &tests.ImportTest, &tests.NeedTest); err != nil {
return err
}
}
if strings.HasPrefix(decl.FullName, testPkg.ImportPath+".Benchmark") {
tests.Benchmarks = append(tests.Benchmarks, testFunc{Package: testPkgName, Name: decl.FullName[len(testPkg.ImportPath)+1:]})
*needVar = true
} else {
for _, file := range pkg.XTestGoFiles {
if err := tests.load(filepath.Join(pkg.Package.Dir, file), "_xtest", &tests.ImportXtest, &tests.NeedXtest); err != nil {
return err
}
}
}
_, err := s.BuildPackage(testPkg)
if err != nil {
return err
}
return nil
}

Expand Down Expand Up @@ -770,12 +777,15 @@ func runNode(script string, args []string, dir string, quiet bool) error {
}

type testFuncs struct {
Tests []testFunc
Benchmarks []testFunc
Examples []testFunc
Package *build.Package
NeedTest bool
NeedXtest bool
Tests []testFunc
Benchmarks []testFunc
Examples []testFunc
TestMain *testFunc
Package *build.Package
ImportTest bool
NeedTest bool
ImportXtest bool
NeedXtest bool
}

type testFunc struct {
Expand All @@ -784,18 +794,111 @@ type testFunc struct {
Output string // output, for examples
}

var testFileSet = token.NewFileSet()

func (t *testFuncs) load(filename, pkg string, doImport, seen *bool) error {
f, err := parser.ParseFile(testFileSet, filename, nil, parser.ParseComments)
if err != nil {
return err
}
for _, d := range f.Decls {
n, ok := d.(*ast.FuncDecl)
if !ok {
continue
}
if n.Recv != nil {
continue
}
name := n.Name.String()
switch {
case isTestMain(n):
if t.TestMain != nil {
return errors.New("multiple definitions of TestMain")
}
t.TestMain = &testFunc{pkg, name, ""}
*doImport, *seen = true, true
case isTest(name, "Test"):
t.Tests = append(t.Tests, testFunc{pkg, name, ""})
*doImport, *seen = true, true
case isTest(name, "Benchmark"):
t.Benchmarks = append(t.Benchmarks, testFunc{pkg, name, ""})
*doImport, *seen = true, true
}
}
// ex := doc.Examples(f)
// sort.Sort(byOrder(ex))
// for _, e := range ex {
// *doImport = true // import test file whether executed or not
// if e.Output == "" && !e.EmptyOutput {
// // Don't run examples with no output.
// continue
// }
// t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output})
// *seen = true
// }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the code to support examples that's commented out because it's blocked on #381, right?

I have a simple request. Either remove the code and just make note of it in the PR (in a comment or so), so we can look it up and add it later after #381 is resolved.

Either that, or, if you want to keep it, then please add a comment explaining why this is commented out. E.g.:

// This is the code to support examples. It's blocked on https://github.com/gopherjs/gopherjs/issues/381 being resolved.
/*ex := doc.Examples(f)
sort.Sort(byOrder(ex))
for _, e := range ex {
    *doImport = true // import test file whether executed or not
    if e.Output == "" && !e.EmptyOutput {
        // Don't run examples with no output.
        continue
    }
    t.Examples = append(t.Examples, testFunc{pkg, "Example" + e.Name, e.Output})
    *seen = true
}*/

If you're indifferent, I prefer removing it (just make sure a copy of it is accessible somewhere in this PR for lookup at a later time).

return nil
}

type byOrder []*doc.Example

func (x byOrder) Len() int { return len(x) }
func (x byOrder) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
func (x byOrder) Less(i, j int) bool { return x[i].Order < x[j].Order }

// isTestMain tells whether fn is a TestMain(m *testing.M) function.
func isTestMain(fn *ast.FuncDecl) bool {
if fn.Name.String() != "TestMain" ||
fn.Type.Results != nil && len(fn.Type.Results.List) > 0 ||
fn.Type.Params == nil ||
len(fn.Type.Params.List) != 1 ||
len(fn.Type.Params.List[0].Names) > 1 {
return false
}
ptr, ok := fn.Type.Params.List[0].Type.(*ast.StarExpr)
if !ok {
return false
}
// We can't easily check that the type is *testing.M
// because we don't know how testing has been imported,
// but at least check that it's *M or *something.M.
if name, ok := ptr.X.(*ast.Ident); ok && name.Name == "M" {
return true
}
if sel, ok := ptr.X.(*ast.SelectorExpr); ok && sel.Sel.Name == "M" {
return true
}
return false
}

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

var testmainTmpl = template.Must(template.New("main").Parse(`
package main

import (
{{if not .TestMain}}
"os"
{{end}}
"regexp"
"testing"

{{if .NeedTest}}
_test {{.Package.ImportPath | printf "%q"}}
{{if .ImportTest}}
{{if .NeedTest}}_test{{else}}_{{end}} {{.Package.ImportPath | printf "%q"}}
{{end}}
{{if .NeedXtest}}
_xtest {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{if .ImportXtest}}
{{if .NeedXtest}}_xtest{{else}}_{{end}} {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
{{end}}
)

Expand Down Expand Up @@ -832,7 +935,12 @@ func matchString(pat, str string) (result bool, err error) {
}

func main() {
testing.Main(matchString, tests, benchmarks, examples)
m := testing.MainStart(matchString, tests, benchmarks, examples)
{{with .TestMain}}
{{.Package}}.{{.Name}}(m)
{{else}}
os.Exit(m.Run())
{{end}}
}

`))