From 936fd6dadcaa3971e88526cde2e2b4a09f5b16fe Mon Sep 17 00:00:00 2001 From: gedi Date: Sun, 22 May 2016 23:27:45 +0300 Subject: [PATCH 1/8] build test binary instead of single file to execute, use TestMain --- builder.go | 335 ++++++++++++++-------------------------------- builder_test.go | 227 ------------------------------- cmd/godog/main.go | 29 ++-- run.go | 9 +- suite_test.go | 8 ++ 5 files changed, 130 insertions(+), 478 deletions(-) diff --git a/builder.go b/builder.go index 915e3f4..070acad 100644 --- a/builder.go +++ b/builder.go @@ -10,17 +10,17 @@ import ( "os" "path" "path/filepath" - "strconv" "strings" "text/template" ) type builder struct { - files map[string]*ast.File - fset *token.FileSet - Contexts []string - Internal bool - tpl *template.Template + files map[string]*ast.File + fset *token.FileSet + Contexts []string + Internal bool + SuiteName string + tpl *template.Template imports []*ast.ImportSpec } @@ -46,22 +46,26 @@ func newBuilderSkel() *builder { files: make(map[string]*ast.File), fset: token.NewFileSet(), tpl: template.Must(template.New("main").Parse(`package main -{{ if not .Internal }}import ( - "github.com/DATA-DOG/godog" -){{ end }} +import ( +{{ if not .Internal }} "github.com/DATA-DOG/godog"{{ end }} + "os" + "testing" +) -func main() { +const GodogSuiteName = "{{ .SuiteName }}" - {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) { +func TestMain(m *testing.M) { + status := {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) { {{range .Contexts}} {{ . }}(suite) {{end}} }) + os.Exit(status) }`)), } } -func newBuilder(buildPath string) (*builder, error) { +func doBuild(buildPath, dir string) error { b := newBuilderSkel() err := filepath.Walk(buildPath, func(path string, file os.FileInfo, err error) error { if file.IsDir() && file.Name() != "." { @@ -72,23 +76,89 @@ func newBuilder(buildPath string) (*builder, error) { if err != nil { return err } - b.register(f, path) + b.register(f, file.Name()) } return err }) - return b, err + if err != nil { + return err + } + + var buf bytes.Buffer + if err := b.tpl.Execute(&buf, b); err != nil { + return err + } + + f, err := parser.ParseFile(b.fset, "", &buf, 0) + if err != nil { + return err + } + + b.files["godog_test.go"] = f + + os.Mkdir(dir, 0755) + for name, node := range b.files { + f, err := os.Create(filepath.Join(dir, name)) + if err != nil { + return err + } + if err := format.Node(f, b.fset, node); err != nil { + return err + } + } + + return nil } -func (b *builder) register(f *ast.File, path string) { +func (b *builder) register(f *ast.File, name string) { // mark godog package as internal if f.Name.Name == "godog" && !b.Internal { b.Internal = true } + b.SuiteName = f.Name.Name b.deleteMainFunc(f) + f.Name.Name = "main" b.registerContexts(f) - b.deleteImports(f) - b.files[path] = f + b.files[name] = f +} + +func (b *builder) removeUnusedImports(f *ast.File) { + used := b.usedPackages(f) + isUsed := func(p string) bool { + for _, ref := range used { + if p == ref { + return true + } + } + return p == "_" + } + var decls []ast.Decl + for _, d := range f.Decls { + gen, ok := d.(*ast.GenDecl) + if ok && gen.Tok == token.IMPORT { + var specs []ast.Spec + for _, spec := range gen.Specs { + impspec := spec.(*ast.ImportSpec) + ipath := strings.Trim(impspec.Path.Value, `\"`) + check := importPathToName(ipath) + if impspec.Name != nil { + check = impspec.Name.Name + } + + if isUsed(check) { + specs = append(specs, spec) + } + } + + if len(specs) == 0 { + continue + } + gen.Specs = specs + } + decls = append(decls, d) + } + f.Decls = decls } func (b *builder) deleteImports(f *ast.File) { @@ -111,17 +181,24 @@ func (b *builder) deleteImports(f *ast.File) { func (b *builder) deleteMainFunc(f *ast.File) { var decls []ast.Decl + var hadMain bool for _, d := range f.Decls { fun, ok := d.(*ast.FuncDecl) if !ok { decls = append(decls, d) continue } - if fun.Name.Name != "main" { + if fun.Name.Name != "TestMain" { decls = append(decls, fun) + } else { + hadMain = true } } f.Decls = decls + + if hadMain { + b.removeUnusedImports(f) + } } func (b *builder) registerContexts(f *ast.File) { @@ -181,68 +258,6 @@ func (b *builder) usedPackages(f *ast.File) []string { return refs } -func (b *builder) merge() ([]byte, error) { - var buf bytes.Buffer - if err := b.tpl.Execute(&buf, b); err != nil { - return nil, err - } - - f, err := parser.ParseFile(b.fset, "", &buf, 0) - if err != nil { - return nil, err - } - b.deleteImports(f) - b.files["main.go"] = f - - pkg, _ := ast.NewPackage(b.fset, b.files, nil, nil) - pkg.Name = "main" - - ret, err := ast.MergePackageFiles(pkg, 0), nil - if err != nil { - return nil, err - } - - // @TODO: we reread the file, probably something goes wrong with position - buf.Reset() - if err = format.Node(&buf, b.fset, ret); err != nil { - return nil, err - } - - ret, err = parser.ParseFile(b.fset, "", buf.Bytes(), 0) - if err != nil { - return nil, err - } - - used := b.usedPackages(ret) - isUsed := func(p string) bool { - for _, ref := range used { - if p == ref { - return true - } - } - return p == "_" - } - for _, spec := range b.imports { - var name string - ipath := strings.Trim(spec.Path.Value, `\"`) - check := importPathToName(ipath) - if spec.Name != nil { - name = spec.Name.Name - check = spec.Name.Name - } - if isUsed(check) { - addImport(b.fset, ret, name, ipath) - } - } - - buf.Reset() - if err := format.Node(&buf, b.fset, ret); err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - // Build creates a runnable Godog executable file // from current package source and test source files. // @@ -253,165 +268,15 @@ func (b *builder) merge() ([]byte, error) { // Currently, to manage imports we use "golang.org/x/tools/imports" // package, but that may be replaced in order to have // no external dependencies -func Build() ([]byte, error) { - b, err := newBuilder(".") - if err != nil { - return nil, err - } - - return b.merge() +func Build(dir string) error { + return doBuild(".", dir) } -// taken from https://github.com/golang/tools/blob/master/go/ast/astutil/imports.go#L17 -func addImport(fset *token.FileSet, f *ast.File, name, ipath string) { - newImport := &ast.ImportSpec{ - Path: &ast.BasicLit{ - Kind: token.STRING, - Value: strconv.Quote(ipath), - }, - } - if name != "" { - newImport.Name = &ast.Ident{Name: name} - } - - // Find an import decl to add to. - // The goal is to find an existing import - // whose import path has the longest shared - // prefix with ipath. - var ( - bestMatch = -1 // length of longest shared prefix - lastImport = -1 // index in f.Decls of the file's final import decl - impDecl *ast.GenDecl // import decl containing the best match - impIndex = -1 // spec index in impDecl containing the best match - ) - for i, decl := range f.Decls { - gen, ok := decl.(*ast.GenDecl) - if ok && gen.Tok == token.IMPORT { - lastImport = i - // Do not add to import "C", to avoid disrupting the - // association with its doc comment, breaking cgo. - if declImports(gen, "C") { - continue - } - - // Match an empty import decl if that's all that is available. - if len(gen.Specs) == 0 && bestMatch == -1 { - impDecl = gen - } - - // Compute longest shared prefix with imports in this group. - for j, spec := range gen.Specs { - impspec := spec.(*ast.ImportSpec) - n := matchLen(importPath(impspec), ipath) - if n > bestMatch { - bestMatch = n - impDecl = gen - impIndex = j - } - } - } - } - - // If no import decl found, add one after the last import. - if impDecl == nil { - impDecl = &ast.GenDecl{ - Tok: token.IMPORT, - } - if lastImport >= 0 { - impDecl.TokPos = f.Decls[lastImport].End() - } else { - // There are no existing imports. - // Our new import goes after the package declaration and after - // the comment, if any, that starts on the same line as the - // package declaration. - impDecl.TokPos = f.Package - - file := fset.File(f.Package) - pkgLine := file.Line(f.Package) - for _, c := range f.Comments { - if file.Line(c.Pos()) > pkgLine { - break - } - impDecl.TokPos = c.End() - } - } - f.Decls = append(f.Decls, nil) - copy(f.Decls[lastImport+2:], f.Decls[lastImport+1:]) - f.Decls[lastImport+1] = impDecl - } - - // Insert new import at insertAt. - insertAt := 0 - if impIndex >= 0 { - // insert after the found import - insertAt = impIndex + 1 - } - impDecl.Specs = append(impDecl.Specs, nil) - copy(impDecl.Specs[insertAt+1:], impDecl.Specs[insertAt:]) - impDecl.Specs[insertAt] = newImport - pos := impDecl.Pos() - if insertAt > 0 { - // Assign same position as the previous import, - // so that the sorter sees it as being in the same block. - pos = impDecl.Specs[insertAt-1].Pos() - } - if newImport.Name != nil { - newImport.Name.NamePos = pos - } - newImport.Path.ValuePos = pos - newImport.EndPos = pos - - // Clean up parens. impDecl contains at least one spec. - if len(impDecl.Specs) == 1 { - // Remove unneeded parens. - impDecl.Lparen = token.NoPos - } else if !impDecl.Lparen.IsValid() { - // impDecl needs parens added. - impDecl.Lparen = impDecl.Specs[0].Pos() - } - - f.Imports = append(f.Imports, newImport) -} - -func declImports(gen *ast.GenDecl, path string) bool { - if gen.Tok != token.IMPORT { - return false - } - for _, spec := range gen.Specs { - impspec := spec.(*ast.ImportSpec) - if importPath(impspec) == path { - return true - } - } - return false -} - -func matchLen(x, y string) int { - n := 0 - for i := 0; i < len(x) && i < len(y) && x[i] == y[i]; i++ { - if x[i] == '/' { - n++ - } - } - return n -} - -func importPath(s *ast.ImportSpec) string { - return strings.Trim(s.Path.Value, `\"`) -} - -var importPathToName = importPathToNameGoPath - -// importPathToNameBasic assumes the package name is the base of import path. -func importPathToNameBasic(importPath string) (packageName string) { - return path.Base(importPath) -} - -// importPathToNameGoPath finds out the actual package name, as declared in its .go files. +// importPathToName finds out the actual package name, as declared in its .go files. // If there's a problem, it falls back to using importPathToNameBasic. -func importPathToNameGoPath(importPath string) (packageName string) { +func importPathToName(importPath string) (packageName string) { if buildPkg, err := build.Import(importPath, "", 0); err == nil { return buildPkg.Name } - return importPathToNameBasic(importPath) + return path.Base(importPath) } diff --git a/builder_test.go b/builder_test.go index 6338874..5d7c8c5 100644 --- a/builder_test.go +++ b/builder_test.go @@ -1,228 +1 @@ package godog - -import ( - "fmt" - "go/parser" - "go/token" - "runtime" - "strings" - "testing" -) - -var builderMainFile = ` -package main -import "fmt" -func main() { - fmt.Println("hello") -}` - -var builderPackAliases = ` -package main -import ( - a "fmt" - b "fmt" -) -func Tester() { - a.Println("a") - b.Println("b") -}` - -var builderAnonymousImport = ` -package main -import ( - _ "github.com/go-sql-driver/mysql" -) -` - -var builderContextSrc = ` -package main -import ( - "github.com/DATA-DOG/godog" -) - -func myContext(s *godog.Suite) { - -} -` - -var builderLibrarySrc = ` -package lib -import "fmt" -func test() { - fmt.Println("hello") -} -` - -var builderInternalPackageSrc = ` -package godog -import "fmt" -func test() { - fmt.Println("hello") -} -` - -func (b *builder) registerMulti(contents []string) error { - for i, c := range contents { - f, err := parser.ParseFile(token.NewFileSet(), "", []byte(c), 0) - if err != nil { - return err - } - b.register(f, fmt.Sprintf("path%d", i)) - } - return nil -} - -func (b *builder) cleanSpacing(src string) string { - var lines []string - for _, ln := range strings.Split(src, "\n") { - if ln == "" { - continue - } - lines = append(lines, strings.TrimSpace(ln)) - } - return strings.Join(lines, "\n") -} - -func TestUsualSourceFileMerge(t *testing.T) { - if strings.HasPrefix(runtime.Version(), "go1.1") { - t.Skip("skipping this test for go1.1") - } - b := newBuilderSkel() - err := b.registerMulti([]string{ - builderMainFile, builderPackAliases, builderAnonymousImport, - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - data, err := b.merge() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - expected := `package main - -import ( - a "fmt" - b "fmt" - "github.com/DATA-DOG/godog" - _ "github.com/go-sql-driver/mysql" -) - -func main() { - godog.Run(func(suite *godog.Suite) { - - }) -} -func Tester() { - a.Println("a") - b.Println("b") -}` - - actual := string(data) - if b.cleanSpacing(expected) != b.cleanSpacing(actual) { - t.Fatalf("expected output does not match: %s", actual) - } -} - -func TestShouldCallContextOnMerged(t *testing.T) { - b := newBuilderSkel() - err := b.registerMulti([]string{ - builderMainFile, builderContextSrc, - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - data, err := b.merge() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - expected := `package main - -import "github.com/DATA-DOG/godog" - -func main() { - godog.Run(func(suite *godog.Suite) { - myContext(suite) - }) -} - -func myContext(s *godog.Suite) { -}` - - actual := string(data) - // log.Println("actual:", actual) - // log.Println("expected:", expected) - if b.cleanSpacing(expected) != b.cleanSpacing(actual) { - t.Fatalf("expected output does not match: %s", actual) - } -} - -func TestBuildLibraryPackage(t *testing.T) { - b := newBuilderSkel() - err := b.registerMulti([]string{ - builderLibrarySrc, - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - data, err := b.merge() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - expected := `package main -import ( - "fmt" - "github.com/DATA-DOG/godog" -) - -func main() { - godog.Run(func(suite *godog.Suite) { - - }) -} - -func test() { - fmt.Println( - "hello", - ) -}` - - actual := string(data) - if b.cleanSpacing(expected) != b.cleanSpacing(actual) { - t.Fatalf("expected output does not match: %s", actual) - } -} - -func TestBuildInternalPackage(t *testing.T) { - b := newBuilderSkel() - err := b.registerMulti([]string{ - builderInternalPackageSrc, - }) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - data, err := b.merge() - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - expected := `package main -import "fmt" - -func main() { - Run(func(suite *Suite) { - - }) -} - -func test() { - fmt.Println("hello") -}` - - actual := string(data) - if b.cleanSpacing(expected) != b.cleanSpacing(actual) { - t.Fatalf("expected output does not match: %s", actual) - } -} diff --git a/cmd/godog/main.go b/cmd/godog/main.go index 94032e2..eb15bee 100644 --- a/cmd/godog/main.go +++ b/cmd/godog/main.go @@ -3,8 +3,10 @@ package main import ( "fmt" "io" + "log" "os" "os/exec" + "path/filepath" "regexp" "strconv" "syscall" @@ -23,26 +25,29 @@ func buildAndRun() (int, error) { stdout := ansicolor.NewAnsiColorWriter(os.Stdout) stderr := ansicolor.NewAnsiColorWriter(statusOutputFilter(os.Stderr)) - builtFile := fmt.Sprintf("%s/%dgodog.go", os.TempDir(), time.Now().UnixNano()) - - buf, err := godog.Build() + dir := fmt.Sprintf(filepath.Join("%s", "%dgodogs"), os.TempDir(), time.Now().UnixNano()) + err := godog.Build(dir) if err != nil { - return status, err + return 1, err } - w, err := os.Create(builtFile) + defer os.RemoveAll(dir) + + wd, err := os.Getwd() if err != nil { - return status, err + return 1, err } - defer os.Remove(builtFile) + bin := filepath.Join(wd, "godog.test") - if _, err = w.Write(buf); err != nil { - w.Close() - return status, err + cmdb := exec.Command("go", "test", "-c", "-o", bin) + cmdb.Dir = dir + if dat, err := cmdb.CombinedOutput(); err != nil { + log.Println(string(dat)) + return 1, err } - w.Close() + defer os.Remove(bin) - cmd := exec.Command("go", append([]string{"run", builtFile}, os.Args[1:]...)...) + cmd := exec.Command(bin, os.Args[1:]...) cmd.Stdout = stdout cmd.Stderr = stderr diff --git a/run.go b/run.go index 9b6e404..b89855f 100644 --- a/run.go +++ b/run.go @@ -52,7 +52,7 @@ func (r *runner) run() (failed bool) { // // contextInitializer must be able to register // the step definitions and event handlers. -func Run(contextInitializer func(suite *Suite)) { +func Run(contextInitializer func(suite *Suite)) int { var vers, defs, sof bool var tags, format string var concurrency int @@ -63,12 +63,12 @@ func Run(contextInitializer func(suite *Suite)) { switch { case vers: fmt.Println(cl("Godog", green) + " version is " + cl(Version, yellow)) - return + return 0 case defs: s := &Suite{} contextInitializer(s) s.printStepDefinitions() - return + return 0 } paths := flagSet.Args() @@ -99,6 +99,7 @@ func Run(contextInitializer func(suite *Suite)) { } if failed := r.run(); failed { - os.Exit(1) + return 1 } + return 0 } diff --git a/suite_test.go b/suite_test.go index ba37e6e..c8fb7d1 100644 --- a/suite_test.go +++ b/suite_test.go @@ -1,13 +1,21 @@ package godog import ( + "flag" "fmt" + "os" "strconv" "strings" + "testing" "gopkg.in/cucumber/gherkin-go.v3" ) +func TestMain(m *testing.M) { + flag.Parse() + os.Exit(m.Run()) +} + func SuiteContext(s *Suite) { c := &suiteContext{} From 4c779c8fdf865844ca17910974a011f78ea35fd8 Mon Sep 17 00:00:00 2001 From: gedi Date: Sun, 22 May 2016 23:35:20 +0300 Subject: [PATCH 2/8] drop support for older than 1.4 go versions --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 04cab03..6816451 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,5 @@ language: go go: - - 1.1 - - 1.2 - - 1.3 - 1.4 - 1.5 - 1.6 @@ -14,6 +11,8 @@ script: - go get github.com/shiena/ansicolor # run standard go tests + - go vet ./... + - go fmt ./... - go test -v - go test -race From 8cd540d3afed4f789d3aa50a6958c614f5363804 Mon Sep 17 00:00:00 2001 From: gedi Date: Mon, 23 May 2016 17:53:51 +0300 Subject: [PATCH 3/8] reorganize test package builder --- ast.go | 109 ++++++++++++++++++++ ast_test.go | 117 +++++++++++++++++++++ builder.go | 267 +++++++++++------------------------------------- builder_test.go | 1 - 4 files changed, 286 insertions(+), 208 deletions(-) create mode 100644 ast.go create mode 100644 ast_test.go delete mode 100644 builder_test.go diff --git a/ast.go b/ast.go new file mode 100644 index 0000000..eb9af55 --- /dev/null +++ b/ast.go @@ -0,0 +1,109 @@ +package godog + +import ( + "go/ast" + "go/build" + "go/token" + "path" + "strings" +) + +func removeUnusedImports(f *ast.File) { + used := usedPackages(f) + isUsed := func(p string) bool { + for _, ref := range used { + if p == ref { + return true + } + } + return p == "_" + } + var decls []ast.Decl + for _, d := range f.Decls { + gen, ok := d.(*ast.GenDecl) + if ok && gen.Tok == token.IMPORT { + var specs []ast.Spec + for _, spec := range gen.Specs { + impspec := spec.(*ast.ImportSpec) + ipath := strings.Trim(impspec.Path.Value, `\"`) + check := importPathToName(ipath) + if impspec.Name != nil { + check = impspec.Name.Name + } + + if isUsed(check) { + specs = append(specs, spec) + } + } + + if len(specs) == 0 { + continue + } + gen.Specs = specs + } + decls = append(decls, d) + } + f.Decls = decls +} + +func deleteTestMainFunc(f *ast.File) { + var decls []ast.Decl + var hadMain bool + for _, d := range f.Decls { + fun, ok := d.(*ast.FuncDecl) + if !ok { + decls = append(decls, d) + continue + } + if fun.Name.Name != "TestMain" { + decls = append(decls, fun) + } else { + hadMain = true + } + } + f.Decls = decls + + if hadMain { + removeUnusedImports(f) + } +} + +type visitFn func(node ast.Node) ast.Visitor + +func (fn visitFn) Visit(node ast.Node) ast.Visitor { + return fn(node) +} + +func usedPackages(f *ast.File) []string { + var refs []string + var visitor visitFn + visitor = visitFn(func(node ast.Node) ast.Visitor { + if node == nil { + return visitor + } + switch v := node.(type) { + case *ast.SelectorExpr: + xident, ok := v.X.(*ast.Ident) + if !ok { + break + } + if xident.Obj != nil { + // if the parser can resolve it, it's not a package ref + break + } + refs = append(refs, xident.Name) + } + return visitor + }) + ast.Walk(visitor, f) + return refs +} + +// importPathToName finds out the actual package name, as declared in its .go files. +// If there's a problem, it falls back to using importPathToNameBasic. +func importPathToName(importPath string) (packageName string) { + if buildPkg, err := build.Import(importPath, "", 0); err == nil { + return buildPkg.Name + } + return path.Base(importPath) +} diff --git a/ast_test.go b/ast_test.go new file mode 100644 index 0000000..185ac9c --- /dev/null +++ b/ast_test.go @@ -0,0 +1,117 @@ +package godog + +var builderMainFile = ` +package main +import "fmt" +func main() { + fmt.Println("hello") +}` + +var builderTestMainFile = ` +package main +import ( + "fmt" + "testing" + "os" +) +func TestMain(m *testing.M) { + fmt.Println("hello") + os.Exit(0) +}` + +var builderPackAliases = ` +package main +import ( + "testing" + a "fmt" + b "fmt" +) +func TestMain(m *testing.M) { + a.Println("a") + b.Println("b") +}` + +var builderAnonymousImport = ` +package main +import ( + "testing" + _ "github.com/go-sql-driver/mysql" +) +func TestMain(m *testing.M) { +}` + +var builderContextSrc = ` +package main +import ( + "github.com/DATA-DOG/godog" +) +func myContext(s *godog.Suite) { +} +` + +var builderLibrarySrc = ` +package lib +import "fmt" +func test() { + fmt.Println("hello") +} +` + +var builderInternalPackageSrc = ` +package godog +import "fmt" +func test() { + fmt.Println("hello") +} +` + +// func builderProcess(src string, t *testing.T) string { +// fset := token.NewFileSet() +// f, err := parser.ParseFile(fset, "", []byte(builderTestMainFile), 0) +// if err != nil { +// t.Fatalf("unexpected error while parsing ast: %v", err) +// } + +// deleteTestMainFunc(f) + +// var buf strings.Buffer +// if err := format.Node(&buf, fset, node); err != nil { +// return err +// } +// } + +// func TestShouldCleanTestMainFromSimpleTestFile(t *testing.T) { + +// b := newBuilderSkel() +// err := b.registerMulti([]string{ +// builderMainFile, builderPackAliases, builderAnonymousImport, +// }) +// if err != nil { +// t.Fatalf("unexpected error: %s", err) +// } + +// data, err := b.merge() +// if err != nil { +// t.Fatalf("unexpected error: %s", err) +// } +// expected := `package main +// import ( +// a "fmt" +// b "fmt" +// "github.com/DATA-DOG/godog" +// _ "github.com/go-sql-driver/mysql" +// ) +// func main() { +// godog.Run(func(suite *godog.Suite) { +// }) +// } +// func Tester() { +// a.Println("a") +// b.Println("b") +// }` + +// actual := string(data) +// if b.cleanSpacing(expected) != b.cleanSpacing(actual) { +// t.Fatalf("expected output does not match: %s", actual) +// } +// } diff --git a/builder.go b/builder.go index 070acad..5ce5ba0 100644 --- a/builder.go +++ b/builder.go @@ -3,49 +3,16 @@ package godog import ( "bytes" "go/ast" - "go/build" "go/format" "go/parser" "go/token" "os" - "path" "path/filepath" "strings" "text/template" ) -type builder struct { - files map[string]*ast.File - fset *token.FileSet - Contexts []string - Internal bool - SuiteName string - tpl *template.Template - - imports []*ast.ImportSpec -} - -func (b *builder) hasImport(a *ast.ImportSpec) bool { - for _, b := range b.imports { - var aname, bname string - if a.Name != nil { - aname = a.Name.Name - } - if b.Name != nil { - bname = b.Name.Name - } - if bname == aname && a.Path.Value == b.Path.Value { - return true - } - } - return false -} - -func newBuilderSkel() *builder { - return &builder{ - files: make(map[string]*ast.File), - fset: token.NewFileSet(), - tpl: template.Must(template.New("main").Parse(`package main +var runnerTemplate = template.Must(template.New("main").Parse(`package main import ( {{ if not .Internal }} "github.com/DATA-DOG/godog"{{ end }} "os" @@ -61,54 +28,13 @@ func TestMain(m *testing.M) { {{end}} }) os.Exit(status) -}`)), - } -} +}`)) -func doBuild(buildPath, dir string) error { - b := newBuilderSkel() - err := filepath.Walk(buildPath, func(path string, file os.FileInfo, err error) error { - if file.IsDir() && file.Name() != "." { - return filepath.SkipDir - } - if err == nil && strings.HasSuffix(path, ".go") { - f, err := parser.ParseFile(b.fset, path, nil, 0) - if err != nil { - return err - } - b.register(f, file.Name()) - } - return err - }) - - if err != nil { - return err - } - - var buf bytes.Buffer - if err := b.tpl.Execute(&buf, b); err != nil { - return err - } - - f, err := parser.ParseFile(b.fset, "", &buf, 0) - if err != nil { - return err - } - - b.files["godog_test.go"] = f - - os.Mkdir(dir, 0755) - for name, node := range b.files { - f, err := os.Create(filepath.Join(dir, name)) - if err != nil { - return err - } - if err := format.Node(f, b.fset, node); err != nil { - return err - } - } - - return nil +type builder struct { + files map[string]*ast.File + Contexts []string + Internal bool + SuiteName string } func (b *builder) register(f *ast.File, name string) { @@ -117,90 +43,12 @@ func (b *builder) register(f *ast.File, name string) { b.Internal = true } b.SuiteName = f.Name.Name - b.deleteMainFunc(f) + deleteTestMainFunc(f) f.Name.Name = "main" b.registerContexts(f) b.files[name] = f } -func (b *builder) removeUnusedImports(f *ast.File) { - used := b.usedPackages(f) - isUsed := func(p string) bool { - for _, ref := range used { - if p == ref { - return true - } - } - return p == "_" - } - var decls []ast.Decl - for _, d := range f.Decls { - gen, ok := d.(*ast.GenDecl) - if ok && gen.Tok == token.IMPORT { - var specs []ast.Spec - for _, spec := range gen.Specs { - impspec := spec.(*ast.ImportSpec) - ipath := strings.Trim(impspec.Path.Value, `\"`) - check := importPathToName(ipath) - if impspec.Name != nil { - check = impspec.Name.Name - } - - if isUsed(check) { - specs = append(specs, spec) - } - } - - if len(specs) == 0 { - continue - } - gen.Specs = specs - } - decls = append(decls, d) - } - f.Decls = decls -} - -func (b *builder) deleteImports(f *ast.File) { - var decls []ast.Decl - for _, d := range f.Decls { - gen, ok := d.(*ast.GenDecl) - if ok && gen.Tok == token.IMPORT { - for _, spec := range gen.Specs { - impspec := spec.(*ast.ImportSpec) - if !b.hasImport(impspec) { - b.imports = append(b.imports, impspec) - } - } - continue - } - decls = append(decls, d) - } - f.Decls = decls -} - -func (b *builder) deleteMainFunc(f *ast.File) { - var decls []ast.Decl - var hadMain bool - for _, d := range f.Decls { - fun, ok := d.(*ast.FuncDecl) - if !ok { - decls = append(decls, d) - continue - } - if fun.Name.Name != "TestMain" { - decls = append(decls, fun) - } else { - hadMain = true - } - } - f.Decls = decls - - if hadMain { - b.removeUnusedImports(f) - } -} - func (b *builder) registerContexts(f *ast.File) { for _, d := range f.Decls { switch fun := d.(type) { @@ -227,56 +75,61 @@ func (b *builder) registerContexts(f *ast.File) { } } -type visitFn func(node ast.Node) ast.Visitor - -func (fn visitFn) Visit(node ast.Node) ast.Visitor { - return fn(node) -} - -func (b *builder) usedPackages(f *ast.File) []string { - var refs []string - var visitor visitFn - visitor = visitFn(func(node ast.Node) ast.Visitor { - if node == nil { - return visitor - } - switch v := node.(type) { - case *ast.SelectorExpr: - xident, ok := v.X.(*ast.Ident) - if !ok { - break - } - if xident.Obj != nil { - // if the parser can resolve it, it's not a package ref - break - } - refs = append(refs, xident.Name) - } - return visitor - }) - ast.Walk(visitor, f) - return refs -} - -// Build creates a runnable Godog executable file -// from current package source and test source files. +// Build scans all go files in current directory, +// copies them to temporary build directory. +// If there is a TestMain func in any of test.go files +// it removes it and all necessary unused imports related +// to this function. // -// The package files are merged with the help of go/ast into -// a single main package file which has a custom -// main function to run test suite features. +// It also looks for any godog suite contexts and registers +// them in order to call them on execution. // -// Currently, to manage imports we use "golang.org/x/tools/imports" -// package, but that may be replaced in order to have -// no external dependencies +// The test entry point which uses go1.4 TestMain func +// is generated from the template above. func Build(dir string) error { - return doBuild(".", dir) -} + fset := token.NewFileSet() + b := &builder{files: make(map[string]*ast.File)} -// importPathToName finds out the actual package name, as declared in its .go files. -// If there's a problem, it falls back to using importPathToNameBasic. -func importPathToName(importPath string) (packageName string) { - if buildPkg, err := build.Import(importPath, "", 0); err == nil { - return buildPkg.Name + err := filepath.Walk(".", func(path string, file os.FileInfo, err error) error { + if file.IsDir() && file.Name() != "." { + return filepath.SkipDir + } + if err == nil && strings.HasSuffix(path, ".go") { + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + return err + } + b.register(f, file.Name()) + } + return err + }) + + if err != nil { + return err } - return path.Base(importPath) + + var buf bytes.Buffer + if err := runnerTemplate.Execute(&buf, b); err != nil { + return err + } + + f, err := parser.ParseFile(fset, "", &buf, 0) + if err != nil { + return err + } + + b.files["godog_test.go"] = f + + os.Mkdir(dir, 0755) + for name, node := range b.files { + f, err := os.Create(filepath.Join(dir, name)) + if err != nil { + return err + } + if err := format.Node(f, fset, node); err != nil { + return err + } + } + + return nil } diff --git a/builder_test.go b/builder_test.go deleted file mode 100644 index 5d7c8c5..0000000 --- a/builder_test.go +++ /dev/null @@ -1 +0,0 @@ -package godog From 468711f03cf07ff39c5d13c1211d7ebfba5e1bbb Mon Sep 17 00:00:00 2001 From: gedi Date: Tue, 24 May 2016 16:59:33 +0300 Subject: [PATCH 4/8] test builder ast functions --- ast.go | 34 +++++++- ast_context_test.go | 76 +++++++++++++++++ ast_test.go | 199 +++++++++++++++++++++++++++----------------- builder.go | 53 +++--------- 4 files changed, 241 insertions(+), 121 deletions(-) create mode 100644 ast_context_test.go diff --git a/ast.go b/ast.go index eb9af55..3026fda 100644 --- a/ast.go +++ b/ast.go @@ -8,6 +8,34 @@ import ( "strings" ) +func contexts(f *ast.File) []string { + var contexts []string + for _, d := range f.Decls { + switch fun := d.(type) { + case *ast.FuncDecl: + for _, param := range fun.Type.Params.List { + switch expr := param.Type.(type) { + case *ast.StarExpr: + switch x := expr.X.(type) { + case *ast.Ident: + if x.Name == "Suite" { + contexts = append(contexts, fun.Name.Name) + } + case *ast.SelectorExpr: + switch t := x.X.(type) { + case *ast.Ident: + if t.Name == "godog" && x.Sel.Name == "Suite" { + contexts = append(contexts, fun.Name.Name) + } + } + } + } + } + } + } + return contexts +} + func removeUnusedImports(f *ast.File) { used := usedPackages(f) isUsed := func(p string) bool { @@ -48,7 +76,7 @@ func removeUnusedImports(f *ast.File) { func deleteTestMainFunc(f *ast.File) { var decls []ast.Decl - var hadMain bool + var hadTestMain bool for _, d := range f.Decls { fun, ok := d.(*ast.FuncDecl) if !ok { @@ -58,12 +86,12 @@ func deleteTestMainFunc(f *ast.File) { if fun.Name.Name != "TestMain" { decls = append(decls, fun) } else { - hadMain = true + hadTestMain = true } } f.Decls = decls - if hadMain { + if hadTestMain { removeUnusedImports(f) } } diff --git a/ast_context_test.go b/ast_context_test.go new file mode 100644 index 0000000..6b505f3 --- /dev/null +++ b/ast_context_test.go @@ -0,0 +1,76 @@ +package godog + +import ( + "go/parser" + "go/token" + "testing" +) + +var astContextSrc = `package main + +import ( + "github.com/DATA-DOG/godog" +) + +func myContext(s *godog.Suite) { +}` + +var astTwoContextSrc = `package lib + +import ( + "github.com/DATA-DOG/godog" +) + +func apiContext(s *godog.Suite) { +} + +func dbContext(s *godog.Suite) { +}` + +func astContexts(src string, t *testing.T) []string { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", []byte(src), 0) + if err != nil { + t.Fatalf("unexpected error while parsing ast: %v", err) + } + + return contexts(f) +} + +func TestShouldGetSingleContextFromSource(t *testing.T) { + actual := astContexts(astContextSrc, t) + expect := []string{"myContext"} + + if len(actual) != len(expect) { + t.Fatalf("number of found contexts do not match, expected %d, but got %d", len(expect), len(actual)) + } + + for i, c := range expect { + if c != actual[i] { + t.Fatalf("expected context '%s' at pos %d, but got: '%s'", c, i, actual[i]) + } + } +} + +func TestShouldGetTwoContextsFromSource(t *testing.T) { + actual := astContexts(astTwoContextSrc, t) + expect := []string{"apiContext", "dbContext"} + + if len(actual) != len(expect) { + t.Fatalf("number of found contexts do not match, expected %d, but got %d", len(expect), len(actual)) + } + + for i, c := range expect { + if c != actual[i] { + t.Fatalf("expected context '%s' at pos %d, but got: '%s'", c, i, actual[i]) + } + } +} + +func TestShouldNotFindAnyContextsInEmptyFile(t *testing.T) { + actual := astContexts(`package main`, t) + + if len(actual) != 0 { + t.Fatalf("expected no contexts to be found, but there was some: %v", actual) + } +} diff --git a/ast_test.go b/ast_test.go index 185ac9c..1ba1ea8 100644 --- a/ast_test.go +++ b/ast_test.go @@ -1,117 +1,162 @@ package godog -var builderMainFile = ` -package main +import ( + "bytes" + "go/format" + "go/parser" + "go/token" + "strings" + "testing" +) + +var astMainFile = `package main + import "fmt" + func main() { fmt.Println("hello") }` -var builderTestMainFile = ` -package main +var astNormalFile = `package main + +import "fmt" + +func hello() { + fmt.Println("hello") +}` + +var astTestMainFile = `package main + import ( "fmt" "testing" "os" ) + func TestMain(m *testing.M) { fmt.Println("hello") os.Exit(0) }` -var builderPackAliases = ` -package main +var astPackAliases = `package main + import ( "testing" a "fmt" b "fmt" ) + func TestMain(m *testing.M) { a.Println("a") b.Println("b") }` -var builderAnonymousImport = ` -package main +var astAnonymousImport = `package main + import ( "testing" _ "github.com/go-sql-driver/mysql" ) + func TestMain(m *testing.M) { }` -var builderContextSrc = ` -package main +var astLibrarySrc = `package lib + +import "fmt" + +func test() { + fmt.Println("hello") +}` + +var astInternalPackageSrc = `package godog + +import "fmt" + +func test() { + fmt.Println("hello") +}` + +func astProcess(src string, t *testing.T) string { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", []byte(src), 0) + if err != nil { + t.Fatalf("unexpected error while parsing ast: %v", err) + } + + deleteTestMainFunc(f) + + var buf bytes.Buffer + if err := format.Node(&buf, fset, f); err != nil { + t.Fatalf("failed to build source file: %v", err) + } + + return buf.String() +} + +func TestShouldCleanTestMainFromSimpleTestFile(t *testing.T) { + actual := strings.TrimSpace(astProcess(astTestMainFile, t)) + expect := `package main` + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } +} + +func TestShouldCleanTestMainFromFileWithPackageAliases(t *testing.T) { + actual := strings.TrimSpace(astProcess(astPackAliases, t)) + expect := `package main` + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } +} + +func TestShouldNotModifyNormalFile(t *testing.T) { + actual := strings.TrimSpace(astProcess(astNormalFile, t)) + expect := astNormalFile + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } +} + +func TestShouldNotModifyMainFile(t *testing.T) { + actual := strings.TrimSpace(astProcess(astMainFile, t)) + expect := astMainFile + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } +} + +func TestShouldMaintainAnonymousImport(t *testing.T) { + actual := strings.TrimSpace(astProcess(astAnonymousImport, t)) + expect := `package main + import ( - "github.com/DATA-DOG/godog" -) -func myContext(s *godog.Suite) { + _ "github.com/go-sql-driver/mysql" +)` + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } } -` -var builderLibrarySrc = ` -package lib -import "fmt" -func test() { - fmt.Println("hello") +func TestShouldNotModifyLibraryPackageSource(t *testing.T) { + actual := strings.TrimSpace(astProcess(astLibrarySrc, t)) + expect := astLibrarySrc + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } } -` -var builderInternalPackageSrc = ` -package godog -import "fmt" -func test() { - fmt.Println("hello") +func TestShouldNotModifyGodogPackageSource(t *testing.T) { + actual := strings.TrimSpace(astProcess(astInternalPackageSrc, t)) + expect := astInternalPackageSrc + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } } -` - -// func builderProcess(src string, t *testing.T) string { -// fset := token.NewFileSet() -// f, err := parser.ParseFile(fset, "", []byte(builderTestMainFile), 0) -// if err != nil { -// t.Fatalf("unexpected error while parsing ast: %v", err) -// } - -// deleteTestMainFunc(f) - -// var buf strings.Buffer -// if err := format.Node(&buf, fset, node); err != nil { -// return err -// } -// } - -// func TestShouldCleanTestMainFromSimpleTestFile(t *testing.T) { - -// b := newBuilderSkel() -// err := b.registerMulti([]string{ -// builderMainFile, builderPackAliases, builderAnonymousImport, -// }) -// if err != nil { -// t.Fatalf("unexpected error: %s", err) -// } - -// data, err := b.merge() -// if err != nil { -// t.Fatalf("unexpected error: %s", err) -// } -// expected := `package main -// import ( -// a "fmt" -// b "fmt" -// "github.com/DATA-DOG/godog" -// _ "github.com/go-sql-driver/mysql" -// ) -// func main() { -// godog.Run(func(suite *godog.Suite) { -// }) -// } -// func Tester() { -// a.Println("a") -// b.Println("b") -// }` - -// actual := string(data) -// if b.cleanSpacing(expected) != b.cleanSpacing(actual) { -// t.Fatalf("expected output does not match: %s", actual) -// } -// } diff --git a/builder.go b/builder.go index 5ce5ba0..0e81870 100644 --- a/builder.go +++ b/builder.go @@ -12,17 +12,17 @@ import ( "text/template" ) -var runnerTemplate = template.Must(template.New("main").Parse(`package main +var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .PackageName }} import ( -{{ if not .Internal }} "github.com/DATA-DOG/godog"{{ end }} +{{ if ne .PackageName "godog" }} "github.com/DATA-DOG/godog"{{ end }} "os" "testing" ) -const GodogSuiteName = "{{ .SuiteName }}" +const GodogSuiteName = "{{ .PackageName }}" func TestMain(m *testing.M) { - status := {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) { + status := {{ if ne .PackageName "godog" }}godog.{{ end }}Run(func (suite *{{ if ne .PackageName "godog" }}godog.{{ end }}Suite) { {{range .Contexts}} {{ . }}(suite) {{end}} @@ -31,50 +31,19 @@ func TestMain(m *testing.M) { }`)) type builder struct { - files map[string]*ast.File - Contexts []string - Internal bool - SuiteName string + files map[string]*ast.File + Contexts []string + PackageName string } func (b *builder) register(f *ast.File, name string) { - // mark godog package as internal - if f.Name.Name == "godog" && !b.Internal { - b.Internal = true - } - b.SuiteName = f.Name.Name + b.PackageName = f.Name.Name deleteTestMainFunc(f) - f.Name.Name = "main" - b.registerContexts(f) + // f.Name.Name = "main" + b.Contexts = append(b.Contexts, contexts(f)...) b.files[name] = f } -func (b *builder) registerContexts(f *ast.File) { - for _, d := range f.Decls { - switch fun := d.(type) { - case *ast.FuncDecl: - for _, param := range fun.Type.Params.List { - switch expr := param.Type.(type) { - case *ast.StarExpr: - switch x := expr.X.(type) { - case *ast.Ident: - if x.Name == "Suite" { - b.Contexts = append(b.Contexts, fun.Name.Name) - } - case *ast.SelectorExpr: - switch t := x.X.(type) { - case *ast.Ident: - if t.Name == "godog" && x.Sel.Name == "Suite" { - b.Contexts = append(b.Contexts, fun.Name.Name) - } - } - } - } - } - } - } -} - // Build scans all go files in current directory, // copies them to temporary build directory. // If there is a TestMain func in any of test.go files @@ -94,6 +63,8 @@ func Build(dir string) error { if file.IsDir() && file.Name() != "." { return filepath.SkipDir } + // @TODO: maybe should copy all files in root dir (may contain CGO) + // or use build.Import go tool, to manage package details if err == nil && strings.HasSuffix(path, ".go") { f, err := parser.ParseFile(fset, path, nil, 0) if err != nil { From 198755476ddf44cb5ac524bd91a8b03023bb7e10 Mon Sep 17 00:00:00 2001 From: gedi Date: Tue, 24 May 2016 17:42:28 +0300 Subject: [PATCH 5/8] use standard go build package to create a godog test suite * 1d6c8ac finish refactoring to support standard way of building test package --- ast.go | 16 ++-- ast_context_test.go | 10 +-- ast_test.go | 2 +- builder.go | 190 +++++++++++++++++++++++++++++--------------- cmd/godog/main.go | 9 ++- 5 files changed, 145 insertions(+), 82 deletions(-) diff --git a/ast.go b/ast.go index 3026fda..e931ddf 100644 --- a/ast.go +++ b/ast.go @@ -8,7 +8,7 @@ import ( "strings" ) -func contexts(f *ast.File) []string { +func astContexts(f *ast.File) []string { var contexts []string for _, d := range f.Decls { switch fun := d.(type) { @@ -36,8 +36,8 @@ func contexts(f *ast.File) []string { return contexts } -func removeUnusedImports(f *ast.File) { - used := usedPackages(f) +func astRemoveUnusedImports(f *ast.File) { + used := astUsedPackages(f) isUsed := func(p string) bool { for _, ref := range used { if p == ref { @@ -54,7 +54,7 @@ func removeUnusedImports(f *ast.File) { for _, spec := range gen.Specs { impspec := spec.(*ast.ImportSpec) ipath := strings.Trim(impspec.Path.Value, `\"`) - check := importPathToName(ipath) + check := astImportPathToName(ipath) if impspec.Name != nil { check = impspec.Name.Name } @@ -74,7 +74,7 @@ func removeUnusedImports(f *ast.File) { f.Decls = decls } -func deleteTestMainFunc(f *ast.File) { +func astDeleteTestMainFunc(f *ast.File) { var decls []ast.Decl var hadTestMain bool for _, d := range f.Decls { @@ -92,7 +92,7 @@ func deleteTestMainFunc(f *ast.File) { f.Decls = decls if hadTestMain { - removeUnusedImports(f) + astRemoveUnusedImports(f) } } @@ -102,7 +102,7 @@ func (fn visitFn) Visit(node ast.Node) ast.Visitor { return fn(node) } -func usedPackages(f *ast.File) []string { +func astUsedPackages(f *ast.File) []string { var refs []string var visitor visitFn visitor = visitFn(func(node ast.Node) ast.Visitor { @@ -129,7 +129,7 @@ func usedPackages(f *ast.File) []string { // importPathToName finds out the actual package name, as declared in its .go files. // If there's a problem, it falls back to using importPathToNameBasic. -func importPathToName(importPath string) (packageName string) { +func astImportPathToName(importPath string) (packageName string) { if buildPkg, err := build.Import(importPath, "", 0); err == nil { return buildPkg.Name } diff --git a/ast_context_test.go b/ast_context_test.go index 6b505f3..8e1fa20 100644 --- a/ast_context_test.go +++ b/ast_context_test.go @@ -27,18 +27,18 @@ func apiContext(s *godog.Suite) { func dbContext(s *godog.Suite) { }` -func astContexts(src string, t *testing.T) []string { +func astContextParse(src string, t *testing.T) []string { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "", []byte(src), 0) if err != nil { t.Fatalf("unexpected error while parsing ast: %v", err) } - return contexts(f) + return astContexts(f) } func TestShouldGetSingleContextFromSource(t *testing.T) { - actual := astContexts(astContextSrc, t) + actual := astContextParse(astContextSrc, t) expect := []string{"myContext"} if len(actual) != len(expect) { @@ -53,7 +53,7 @@ func TestShouldGetSingleContextFromSource(t *testing.T) { } func TestShouldGetTwoContextsFromSource(t *testing.T) { - actual := astContexts(astTwoContextSrc, t) + actual := astContextParse(astTwoContextSrc, t) expect := []string{"apiContext", "dbContext"} if len(actual) != len(expect) { @@ -68,7 +68,7 @@ func TestShouldGetTwoContextsFromSource(t *testing.T) { } func TestShouldNotFindAnyContextsInEmptyFile(t *testing.T) { - actual := astContexts(`package main`, t) + actual := astContextParse(`package main`, t) if len(actual) != 0 { t.Fatalf("expected no contexts to be found, but there was some: %v", actual) diff --git a/ast_test.go b/ast_test.go index 1ba1ea8..ebf11b5 100644 --- a/ast_test.go +++ b/ast_test.go @@ -84,7 +84,7 @@ func astProcess(src string, t *testing.T) string { t.Fatalf("unexpected error while parsing ast: %v", err) } - deleteTestMainFunc(f) + astDeleteTestMainFunc(f) var buf bytes.Buffer if err := format.Node(&buf, fset, f); err != nil { diff --git a/builder.go b/builder.go index 0e81870..8dfbf17 100644 --- a/builder.go +++ b/builder.go @@ -1,28 +1,27 @@ package godog import ( - "bytes" - "go/ast" + "go/build" "go/format" "go/parser" "go/token" + "io" "os" "path/filepath" - "strings" "text/template" ) -var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .PackageName }} +var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .Name }} import ( -{{ if ne .PackageName "godog" }} "github.com/DATA-DOG/godog"{{ end }} +{{ if ne .Name "godog" }} "github.com/DATA-DOG/godog"{{ end }} "os" "testing" ) -const GodogSuiteName = "{{ .PackageName }}" +const GodogSuiteName = "{{ .Name }}" func TestMain(m *testing.M) { - status := {{ if ne .PackageName "godog" }}godog.{{ end }}Run(func (suite *{{ if ne .PackageName "godog" }}godog.{{ end }}Suite) { + status := {{ if ne .Name "godog" }}godog.{{ end }}Run(func (suite *{{ if ne .Name "godog" }}godog.{{ end }}Suite) { {{range .Contexts}} {{ . }}(suite) {{end}} @@ -30,22 +29,9 @@ func TestMain(m *testing.M) { os.Exit(status) }`)) -type builder struct { - files map[string]*ast.File - Contexts []string - PackageName string -} - -func (b *builder) register(f *ast.File, name string) { - b.PackageName = f.Name.Name - deleteTestMainFunc(f) - // f.Name.Name = "main" - b.Contexts = append(b.Contexts, contexts(f)...) - b.files[name] = f -} - -// Build scans all go files in current directory, -// copies them to temporary build directory. +// Build scans clones current package into a temporary +// godog suite test package. +// // If there is a TestMain func in any of test.go files // it removes it and all necessary unused imports related // to this function. @@ -56,51 +42,127 @@ func (b *builder) register(f *ast.File, name string) { // The test entry point which uses go1.4 TestMain func // is generated from the template above. func Build(dir string) error { - fset := token.NewFileSet() - b := &builder{files: make(map[string]*ast.File)} + pkg, err := build.ImportDir(".", 0) + if err != nil { + return err + } - err := filepath.Walk(".", func(path string, file os.FileInfo, err error) error { - if file.IsDir() && file.Name() != "." { - return filepath.SkipDir - } - // @TODO: maybe should copy all files in root dir (may contain CGO) - // or use build.Import go tool, to manage package details - if err == nil && strings.HasSuffix(path, ".go") { - f, err := parser.ParseFile(fset, path, nil, 0) - if err != nil { + return buildTestPackage(pkg, dir) +} + +// buildTestPackage clones a package and adds a godog +// entry point with TestMain func in order to +// run the test suite. If TestMain func is found in tested +// source, it will be removed so it can be replaced +func buildTestPackage(pkg *build.Package, dir string) error { + // these file packs may be adjusted in the future, if there are complaints + // in general, most important aspect is to replicate go test behavior + err := copyNonTestPackageFiles( + dir, + pkg.CFiles, + pkg.CgoFiles, + pkg.CXXFiles, + pkg.HFiles, + pkg.GoFiles, + pkg.IgnoredGoFiles, + ) + + if err != nil { + return err + } + + contexts, err := processPackageTestFiles(dir, pkg.TestGoFiles, pkg.XTestGoFiles) + if err != nil { + return err + } + + // build godog runner test file + out, err := os.Create(filepath.Join(dir, "godog_runner_test.go")) + if err != nil { + return err + } + defer out.Close() + + data := struct { + Name string + Contexts []string + }{pkg.Name, contexts} + + return runnerTemplate.Execute(out, data) +} + +// copyNonTestPackageFiles simply copies all given file packs +// to the destDir. +func copyNonTestPackageFiles(destDir string, packs ...[]string) error { + for _, pack := range packs { + for _, file := range pack { + if err := copyPackageFile(file, filepath.Join(destDir, file)); err != nil { return err } - b.register(f, file.Name()) - } - return err - }) - - if err != nil { - return err - } - - var buf bytes.Buffer - if err := runnerTemplate.Execute(&buf, b); err != nil { - return err - } - - f, err := parser.ParseFile(fset, "", &buf, 0) - if err != nil { - return err - } - - b.files["godog_test.go"] = f - - os.Mkdir(dir, 0755) - for name, node := range b.files { - f, err := os.Create(filepath.Join(dir, name)) - if err != nil { - return err - } - if err := format.Node(f, fset, node); err != nil { - return err } } - return nil } + +// processPackageTestFiles runs through ast of each test +// file pack and removes TestMain func if found. it also +// looks for godog suite contexts to register on run +func processPackageTestFiles(destDir string, packs ...[]string) ([]string, error) { + var ctxs []string + fset := token.NewFileSet() + for _, pack := range packs { + for _, testFile := range pack { + node, err := parser.ParseFile(fset, testFile, nil, 0) + if err != nil { + return ctxs, err + } + + astDeleteTestMainFunc(node) + ctxs = append(ctxs, astContexts(node)...) + + destFile := filepath.Join(destDir, testFile) + if err = os.MkdirAll(filepath.Dir(destFile), 0755); err != nil { + return ctxs, err + } + + out, err := os.Create(destFile) + if err != nil { + return ctxs, err + } + defer out.Close() + + if err := format.Node(out, fset, node); err != nil { + return ctxs, err + } + } + } + return ctxs, nil +} + +// copyPackageFile simply copies the file, if dest file dir does +// not exist it creates it +func copyPackageFile(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return + } + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr + } + }() + if _, err = io.Copy(out, in); err != nil { + return + } + err = out.Sync() + return +} diff --git a/cmd/godog/main.go b/cmd/godog/main.go index eb15bee..b776bee 100644 --- a/cmd/godog/main.go +++ b/cmd/godog/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "os" "os/exec" "path/filepath" @@ -25,7 +24,7 @@ func buildAndRun() (int, error) { stdout := ansicolor.NewAnsiColorWriter(os.Stdout) stderr := ansicolor.NewAnsiColorWriter(statusOutputFilter(os.Stderr)) - dir := fmt.Sprintf(filepath.Join("%s", "%dgodogs"), os.TempDir(), time.Now().UnixNano()) + dir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano()) err := godog.Build(dir) if err != nil { return 1, err @@ -41,15 +40,17 @@ func buildAndRun() (int, error) { cmdb := exec.Command("go", "test", "-c", "-o", bin) cmdb.Dir = dir + cmdb.Env = os.Environ() if dat, err := cmdb.CombinedOutput(); err != nil { - log.Println(string(dat)) - return 1, err + fmt.Println(string(dat)) + return 1, nil } defer os.Remove(bin) cmd := exec.Command(bin, os.Args[1:]...) cmd.Stdout = stdout cmd.Stderr = stderr + cmd.Env = os.Environ() if err = cmd.Start(); err != nil { return status, err From 0820626eee02e25edbcb0d6be2c5dc6ccfefffa2 Mon Sep 17 00:00:00 2001 From: gedi Date: Tue, 24 May 2016 23:59:57 +0300 Subject: [PATCH 6/8] update readme --- README.md | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 37342f3..7a45b79 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,27 @@ **The API is likely to change a few times before we reach 1.0.0** -**Godog** is an open source behavior-driven development framework for [go][golang] programming language. -What is behavior-driven development, you ask? It’s the idea that you start by writing human-readable sentences that -describe a feature of your application and how it should work, and only then implement this behavior in software. +**Godog** is an open source behavior-driven development framework for +[go][golang] programming language. What is behavior-driven development, +you ask? It’s the idea that you start by writing human-readable sentences +that describe a feature of your application and how it should work, and +only then implement this behavior in software. -The project is inspired by [behat][behat] and [cucumber][cucumber] and is based on cucumber [gherkin3 parser][gherkin]. +The project is inspired by [behat][behat] and [cucumber][cucumber] and is +based on cucumber [gherkin3 parser][gherkin]. -**Godog** does not intervene with the standard **go test** command and its behavior. You can leverage both frameworks -to functionally test your application while maintaining all test related source code in **_test.go** files. +**Godog** does not intervene with the standard **go test** command and its +behavior. You can leverage both frameworks to functionally test your +application while maintaining all test related source code in **_test.go** +files. -**Godog** acts similar compared to **go test** command. It builds all package sources to a single main package file -and replaces **main** func with its own and runs the build to test described application behavior in feature files. -Production builds remain clean without any test related source code. +**Godog** acts similar compared to **go test** command. It uses +a **TestMain** hook introduced in `go1.4` and clones the package sources +to a temporary build directory. The only change it does is adding a runner +test.go file additionally and ensures to cleanup TestMain func if it was +used in tests. **Godog** uses standard **go** ast and build utils to +generate test suite package and even builds it with **go test -c** +command. It even passes all your environment exported vars. ### Install @@ -144,6 +153,13 @@ See implementation examples: ### Changes +**2016-05-25** +- refactored test suite build tooling in order to use standard **go test** + tool. Which allows to compile package with godog runner script in **go** + idiomatic way. It also supports all build environment options as usual. +- **godog.Run** now returns an **int** exit status. It was not returning + anything before, so there is no compatibility breaks. + **2016-03-04** - added **junit** compatible output formatter, which prints **xml** results to **os.Stdout** From 70af4fc5a7323583f843344885569d19ca4dad91 Mon Sep 17 00:00:00 2001 From: gedi Date: Wed, 25 May 2016 09:01:42 +0300 Subject: [PATCH 7/8] closes #26 --- builder.go | 13 ++++++++----- flags.go | 6 +++--- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/builder.go b/builder.go index 8dfbf17..8d2fe85 100644 --- a/builder.go +++ b/builder.go @@ -12,8 +12,9 @@ import ( ) var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .Name }} + import ( -{{ if ne .Name "godog" }} "github.com/DATA-DOG/godog"{{ end }} + {{ if ne .Name "godog" }}"github.com/DATA-DOG/godog"{{ end }} "os" "testing" ) @@ -22,9 +23,7 @@ const GodogSuiteName = "{{ .Name }}" func TestMain(m *testing.M) { status := {{ if ne .Name "godog" }}godog.{{ end }}Run(func (suite *{{ if ne .Name "godog" }}godog.{{ end }}Suite) { - {{range .Contexts}} - {{ . }}(suite) - {{end}} + {{range .Contexts}}{{ . }}(suite){{end}} }) os.Exit(status) }`)) @@ -71,7 +70,11 @@ func buildTestPackage(pkg *build.Package, dir string) error { return err } - contexts, err := processPackageTestFiles(dir, pkg.TestGoFiles, pkg.XTestGoFiles) + contexts, err := processPackageTestFiles( + dir, + pkg.TestGoFiles, + pkg.XTestGoFiles, + ) if err != nil { return err } diff --git a/flags.go b/flags.go index 5560ebe..adc77da 100644 --- a/flags.go +++ b/flags.go @@ -32,16 +32,16 @@ func usage() { // --- GENERAL --- fmt.Println(cl("Usage:", yellow)) - fmt.Println(s(2) + "godog [options] []\n") + fmt.Println(s(2) + "godog [options] []\n") // --- ARGUMENTS --- fmt.Println(cl("Arguments:", yellow)) // --> paths - fmt.Println(opt("paths", "Optional path(s) to execute. Can be:")) + fmt.Println(opt("features", "Optional feature(s) to run. Can be:")) fmt.Println(opt("", s(4)+"- dir "+cl("(features/)", yellow))) fmt.Println(opt("", s(4)+"- feature "+cl("(*.feature)", yellow))) fmt.Println(opt("", s(4)+"- scenario at specific line "+cl("(*.feature:10)", yellow))) - fmt.Println(opt("", "If no paths are listed, suite tries "+cl("features", yellow)+" path by default.")) + fmt.Println(opt("", "If no feature paths are listed, suite tries "+cl("features", yellow)+" path by default.")) fmt.Println("") // --- OPTIONS --- From 320a5612c1dedb57eb41c9f92741c6c3026c4d2f Mon Sep 17 00:00:00 2001 From: gedi Date: Wed, 25 May 2016 14:26:56 +0300 Subject: [PATCH 8/8] bump version, update error handling on main command --- cmd/godog/main.go | 9 +++++---- gherkin.go | 1 + godog.go | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/cmd/godog/main.go b/cmd/godog/main.go index b776bee..d1ddc7f 100644 --- a/cmd/godog/main.go +++ b/cmd/godog/main.go @@ -41,9 +41,9 @@ func buildAndRun() (int, error) { cmdb := exec.Command("go", "test", "-c", "-o", bin) cmdb.Dir = dir cmdb.Env = os.Environ() - if dat, err := cmdb.CombinedOutput(); err != nil { - fmt.Println(string(dat)) - return 1, nil + if details, err := cmdb.CombinedOutput(); err != nil { + fmt.Println(string(details)) + return 1, err } defer os.Remove(bin) @@ -78,7 +78,8 @@ func buildAndRun() (int, error) { func main() { status, err := buildAndRun() if err != nil { - panic(err) + fmt.Println(err) + os.Exit(1) } // it might be a case, that status might not be resolved // in some OSes. this is attempt to parse it from stderr diff --git a/gherkin.go b/gherkin.go index 80c6b36..ec45f1b 100644 --- a/gherkin.go +++ b/gherkin.go @@ -4,6 +4,7 @@ import "gopkg.in/cucumber/gherkin-go.v3" // examples is a helper func to cast gherkin.Examples // or gherkin.BaseExamples if its empty +// @TODO: this should go away with gherkin update func examples(ex interface{}) (*gherkin.Examples, bool) { t, ok := ex.(*gherkin.Examples) return t, ok diff --git a/godog.go b/godog.go index 6c26e92..fcf96ed 100644 --- a/godog.go +++ b/godog.go @@ -44,4 +44,4 @@ Godog was inspired by Behat and the above description is taken from it's documen package godog // Version of package - based on Semantic Versioning 2.0.0 http://semver.org/ -const Version = "v0.2.0" +const Version = "v0.4.0"