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 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** diff --git a/ast.go b/ast.go new file mode 100644 index 0000000..e931ddf --- /dev/null +++ b/ast.go @@ -0,0 +1,137 @@ +package godog + +import ( + "go/ast" + "go/build" + "go/token" + "path" + "strings" +) + +func astContexts(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 astRemoveUnusedImports(f *ast.File) { + used := astUsedPackages(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 := astImportPathToName(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 astDeleteTestMainFunc(f *ast.File) { + var decls []ast.Decl + var hadTestMain 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 { + hadTestMain = true + } + } + f.Decls = decls + + if hadTestMain { + astRemoveUnusedImports(f) + } +} + +type visitFn func(node ast.Node) ast.Visitor + +func (fn visitFn) Visit(node ast.Node) ast.Visitor { + return fn(node) +} + +func astUsedPackages(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 astImportPathToName(importPath string) (packageName string) { + if buildPkg, err := build.Import(importPath, "", 0); err == nil { + return buildPkg.Name + } + return path.Base(importPath) +} diff --git a/ast_context_test.go b/ast_context_test.go new file mode 100644 index 0000000..8e1fa20 --- /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 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 astContexts(f) +} + +func TestShouldGetSingleContextFromSource(t *testing.T) { + actual := astContextParse(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 := astContextParse(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 := 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 new file mode 100644 index 0000000..ebf11b5 --- /dev/null +++ b/ast_test.go @@ -0,0 +1,162 @@ +package godog + +import ( + "bytes" + "go/format" + "go/parser" + "go/token" + "strings" + "testing" +) + +var astMainFile = `package main + +import "fmt" + +func main() { + fmt.Println("hello") +}` + +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 astPackAliases = `package main + +import ( + "testing" + a "fmt" + b "fmt" +) + +func TestMain(m *testing.M) { + a.Println("a") + b.Println("b") +}` + +var astAnonymousImport = `package main + +import ( + "testing" + _ "github.com/go-sql-driver/mysql" +) + +func TestMain(m *testing.M) { +}` + +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) + } + + astDeleteTestMainFunc(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/go-sql-driver/mysql" +)` + + if actual != expect { + t.Fatalf("expected output does not match: %s", actual) + } +} + +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) + } +} + +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) + } +} diff --git a/builder.go b/builder.go index 915e3f4..8d2fe85 100644 --- a/builder.go +++ b/builder.go @@ -1,417 +1,171 @@ package godog import ( - "bytes" - "go/ast" "go/build" "go/format" "go/parser" "go/token" + "io" "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 +var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .Name }} - imports []*ast.ImportSpec -} +import ( + {{ if ne .Name "godog" }}"github.com/DATA-DOG/godog"{{ end }} + "os" + "testing" +) -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 -} +const GodogSuiteName = "{{ .Name }}" -func newBuilderSkel() *builder { - return &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 }} - -func main() { - - {{ if not .Internal }}godog.{{ end }}Run(func (suite *{{ if not .Internal }}godog.{{ end }}Suite) { - {{range .Contexts}} - {{ . }}(suite) - {{end}} +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}} }) -}`)), + os.Exit(status) +}`)) + +// 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. +// +// It also looks for any godog suite contexts and registers +// them in order to call them on execution. +// +// The test entry point which uses go1.4 TestMain func +// is generated from the template above. +func Build(dir string) error { + pkg, err := build.ImportDir(".", 0) + if err != nil { + return err } + + return buildTestPackage(pkg, dir) } -func newBuilder(buildPath string) (*builder, 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 { +// 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, path) } - return err - }) - - return b, err -} - -func (b *builder) register(f *ast.File, path string) { - // mark godog package as internal - if f.Name.Name == "godog" && !b.Internal { - b.Internal = true } - b.deleteMainFunc(f) - b.registerContexts(f) - b.deleteImports(f) - b.files[path] = f + return nil } -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) - } +// 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 } - continue - } - decls = append(decls, d) - } - f.Decls = decls -} -func (b *builder) deleteMainFunc(f *ast.File) { - var decls []ast.Decl - for _, d := range f.Decls { - fun, ok := d.(*ast.FuncDecl) - if !ok { - decls = append(decls, d) - continue - } - if fun.Name.Name != "main" { - decls = append(decls, fun) - } - } - f.Decls = decls -} + astDeleteTestMainFunc(node) + ctxs = append(ctxs, astContexts(node)...) -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) - } - } - } - } + 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 } -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 -} - -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) +// 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 nil, err + return } - 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 + defer in.Close() + if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return + } + out, err := os.Create(dst) if err != nil { - return nil, err + return } - - // @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 - } + defer func() { + cerr := out.Close() + if err == nil { + err = cerr } - return p == "_" + }() + if _, err = io.Copy(out, in); err != nil { + return } - 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. -// -// 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. -// -// 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() -} - -// 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. -// If there's a problem, it falls back to using importPathToNameBasic. -func importPathToNameGoPath(importPath string) (packageName string) { - if buildPkg, err := build.Import(importPath, "", 0); err == nil { - return buildPkg.Name - } - return importPathToNameBasic(importPath) + err = out.Sync() + return } diff --git a/builder_test.go b/builder_test.go deleted file mode 100644 index 6338874..0000000 --- a/builder_test.go +++ /dev/null @@ -1,228 +0,0 @@ -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..d1ddc7f 100644 --- a/cmd/godog/main.go +++ b/cmd/godog/main.go @@ -5,6 +5,7 @@ import ( "io" "os" "os/exec" + "path/filepath" "regexp" "strconv" "syscall" @@ -23,28 +24,33 @@ 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", "godog-%d"), 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 + cmdb.Env = os.Environ() + if details, err := cmdb.CombinedOutput(); err != nil { + fmt.Println(string(details)) + 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 + cmd.Env = os.Environ() if err = cmd.Start(); err != nil { return status, err @@ -72,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/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 --- 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" 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{}