From 198755476ddf44cb5ac524bd91a8b03023bb7e10 Mon Sep 17 00:00:00 2001 From: gedi Date: Tue, 24 May 2016 17:42:28 +0300 Subject: [PATCH] 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