Merge pull request #40 from DATA-DOG/build-tools
Revisit godog suite compilation and build tools to support vendoring
Этот коммит содержится в:
коммит
5a17900dba
19 изменённых файлов: 311 добавлений и 474 удалений
|
@ -1,6 +1,5 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- tip
|
||||
|
|
23
README.md
23
README.md
|
@ -24,13 +24,9 @@ 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 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.
|
||||
**Godog** acts similar compared to **go test** command. It uses go
|
||||
compiler and linker tool in order to produce test executable. Godog
|
||||
contexts needs to be exported same as Test functions for go test.
|
||||
|
||||
**Godog** ships gherkin parser dependency as a subpackage. This will
|
||||
ensure that it is always compatible with the installed version of godog.
|
||||
|
@ -111,7 +107,7 @@ Since we need a working implementation, we may start by implementing only what i
|
|||
|
||||
#### Step 3
|
||||
|
||||
We only need a number of **godogs** for now. Let's define steps.
|
||||
We only need a number of **godogs** for now. Lets keep it simple.
|
||||
|
||||
``` go
|
||||
/* file: examples/godogs/godog.go */
|
||||
|
@ -125,7 +121,8 @@ func main() { /* usual main func */ }
|
|||
|
||||
#### Step 4
|
||||
|
||||
Now let's finish our step implementations in order to test our feature requirements:
|
||||
Now lets implement our step definitions, which we can copy from generated
|
||||
console output snippets in order to test our feature requirements:
|
||||
|
||||
``` go
|
||||
/* file: examples/godogs/godog_test.go */
|
||||
|
@ -157,7 +154,7 @@ func thereShouldBeRemaining(remaining int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
|
||||
s.Step(`^I eat (\d+)$`, iEat)
|
||||
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
|
||||
|
@ -193,6 +190,12 @@ See implementation examples:
|
|||
|
||||
### Changes
|
||||
|
||||
**2016-06-14**
|
||||
- godog now uses **go tool compile** and **go tool link** to support
|
||||
vendor directory dependencies. It also compiles test executable the same
|
||||
way as standard **go test** utility. With this change, only go
|
||||
versions from **1.5** are now supported.
|
||||
|
||||
**2016-06-01**
|
||||
- parse flags in main command, to show version and help without needing
|
||||
to compile test package and buildable go sources.
|
||||
|
|
108
ast.go
108
ast.go
|
@ -1,12 +1,6 @@
|
|||
package godog
|
||||
|
||||
import (
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/token"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
import "go/ast"
|
||||
|
||||
func astContexts(f *ast.File) []string {
|
||||
var contexts []string
|
||||
|
@ -35,103 +29,3 @@ func astContexts(f *ast.File) []string {
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
160
ast_test.go
160
ast_test.go
|
@ -1,162 +1,76 @@
|
|||
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
|
||||
var astContextSrc = `package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"os"
|
||||
"github.com/DATA-DOG/godog"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
fmt.Println("hello")
|
||||
os.Exit(0)
|
||||
func MyContext(s *godog.Suite) {
|
||||
}`
|
||||
|
||||
var astPackAliases = `package main
|
||||
var astTwoContextSrc = `package lib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
a "fmt"
|
||||
b "fmt"
|
||||
"github.com/DATA-DOG/godog"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
a.Println("a")
|
||||
b.Println("b")
|
||||
func ApiContext(s *godog.Suite) {
|
||||
}
|
||||
|
||||
func DBContext(s *godog.Suite) {
|
||||
}`
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
return astContexts(f)
|
||||
}
|
||||
|
||||
func TestShouldCleanTestMainFromSimpleTestFile(t *testing.T) {
|
||||
actual := strings.TrimSpace(astProcess(astTestMainFile, t))
|
||||
expect := `package main`
|
||||
func TestShouldGetSingleContextFromSource(t *testing.T) {
|
||||
actual := astContextParse(astContextSrc, t)
|
||||
expect := []string{"MyContext"}
|
||||
|
||||
if actual != expect {
|
||||
t.Fatalf("expected output does not match: %s", actual)
|
||||
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 TestShouldCleanTestMainFromFileWithPackageAliases(t *testing.T) {
|
||||
actual := strings.TrimSpace(astProcess(astPackAliases, t))
|
||||
expect := `package main`
|
||||
func TestShouldGetTwoContextsFromSource(t *testing.T) {
|
||||
actual := astContextParse(astTwoContextSrc, t)
|
||||
expect := []string{"ApiContext", "DBContext"}
|
||||
|
||||
if actual != expect {
|
||||
t.Fatalf("expected output does not match: %s", actual)
|
||||
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 TestShouldNotModifyNormalFile(t *testing.T) {
|
||||
actual := strings.TrimSpace(astProcess(astNormalFile, t))
|
||||
expect := astNormalFile
|
||||
func TestShouldNotFindAnyContextsInEmptyFile(t *testing.T) {
|
||||
actual := astContextParse(`package main`, t)
|
||||
|
||||
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)
|
||||
if len(actual) != 0 {
|
||||
t.Fatalf("expected no contexts to be found, but there was some: %v", actual)
|
||||
}
|
||||
}
|
||||
|
|
353
builder.go
353
builder.go
|
@ -1,118 +1,268 @@
|
|||
package godog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"go/format"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var runnerTemplate = template.Must(template.New("main").Parse(`package {{ .Name }}
|
||||
var compiler = filepath.Join(build.ToolDir, "compile")
|
||||
var linker = filepath.Join(build.ToolDir, "link")
|
||||
var gopaths = filepath.SplitList(build.Default.GOPATH)
|
||||
var goarch = build.Default.GOARCH
|
||||
var goos = build.Default.GOOS
|
||||
|
||||
var godogImportPath = "github.com/DATA-DOG/godog"
|
||||
var runnerTemplate = template.Must(template.New("testmain").Parse(`package main
|
||||
|
||||
import (
|
||||
{{ if ne .Name "godog" }}"github.com/DATA-DOG/godog"{{ end }}
|
||||
"github.com/DATA-DOG/godog"
|
||||
{{if .Contexts}}_test "{{.ImportPath}}"{{end}}
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
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) {
|
||||
func main() {
|
||||
status := godog.Run(func (suite *godog.Suite) {
|
||||
{{range .Contexts}}
|
||||
{{ . }}(suite)
|
||||
_test.{{ . }}(suite)
|
||||
{{end}}
|
||||
})
|
||||
os.Exit(status)
|
||||
}`))
|
||||
|
||||
// Build scans clones current package into a temporary
|
||||
// godog suite test package.
|
||||
// Build creates a test package like go test command.
|
||||
// If there are no go files in tested directory, then
|
||||
// it simply builds a godog executable to scan features.
|
||||
//
|
||||
// If there is a TestMain func in any of test.go files
|
||||
// it removes it and all necessary unused imports related
|
||||
// to this function.
|
||||
// If there are go test files, it first builds a test
|
||||
// package with standard go test command.
|
||||
//
|
||||
// It also looks for any godog suite contexts and registers
|
||||
// them in order to call them on execution.
|
||||
// Finally it generates godog suite executable which
|
||||
// registers exported godog contexts from the test files
|
||||
// of tested package.
|
||||
//
|
||||
// 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)
|
||||
// Returns the path to generated executable
|
||||
func Build() (string, error) {
|
||||
abs, err := filepath.Abs(".")
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
|
||||
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
|
||||
bin := filepath.Join(abs, "godog.test")
|
||||
// suffix with .exe for windows
|
||||
if goos == "windows" {
|
||||
bin += ".exe"
|
||||
}
|
||||
|
||||
contexts, err := processPackageTestFiles(
|
||||
dir,
|
||||
pkg.TestGoFiles,
|
||||
pkg.XTestGoFiles,
|
||||
)
|
||||
// we allow package to be nil, if godog is run only when
|
||||
// there is a feature file in empty directory
|
||||
pkg, _ := build.ImportDir(abs, 0)
|
||||
src, anyContexts, err := buildTestMain(pkg)
|
||||
if err != nil {
|
||||
return err
|
||||
return bin, 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()
|
||||
workdir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano())
|
||||
testdir := workdir
|
||||
|
||||
data := struct {
|
||||
Name string
|
||||
Contexts []string
|
||||
}{pkg.Name, contexts}
|
||||
// if none of test files exist, or there are no contexts found
|
||||
// we will skip test package compilation, since it is useless
|
||||
if anyContexts {
|
||||
// first of all compile test package dependencies
|
||||
// that will save was many compilations for dependencies
|
||||
// go does it better
|
||||
out, err := exec.Command("go", "test", "-i").CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, fmt.Errorf("failed to compile package %s:\n%s", pkg.Name, string(out))
|
||||
}
|
||||
|
||||
return runnerTemplate.Execute(out, data)
|
||||
}
|
||||
// let go do the dirty work and compile test
|
||||
// package with it's dependencies. Older go
|
||||
// versions does not accept existing file output
|
||||
// so we create a temporary executable which will
|
||||
// removed.
|
||||
temp := fmt.Sprintf(filepath.Join("%s", "temp-%d.test"), os.TempDir(), time.Now().UnixNano())
|
||||
|
||||
// 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
|
||||
}
|
||||
// builds and compile the tested package.
|
||||
// generated test executable will be removed
|
||||
// since we do not need it for godog suite.
|
||||
// we also print back the temp WORK directory
|
||||
// go has built. We will reuse it for our suite workdir.
|
||||
out, err = exec.Command("go", "test", "-c", "-work", "-o", temp).CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, fmt.Errorf("failed to compile tested package %s:\n%s", pkg.Name, string(out))
|
||||
}
|
||||
defer os.Remove(temp)
|
||||
|
||||
// extract go-build temporary directory as our workdir
|
||||
workdir = strings.TrimSpace(string(out))
|
||||
if !strings.HasPrefix(workdir, "WORK=") {
|
||||
return bin, fmt.Errorf("expected WORK dir path, but got: %s", workdir)
|
||||
}
|
||||
workdir = strings.Replace(workdir, "WORK=", "", 1)
|
||||
testdir = filepath.Join(workdir, pkg.ImportPath, "_test")
|
||||
} else {
|
||||
// still need to create temporary workdir
|
||||
if err = os.MkdirAll(testdir, 0755); err != nil {
|
||||
return bin, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
defer os.RemoveAll(workdir)
|
||||
|
||||
// replace _testmain.go file with our own
|
||||
testmain := filepath.Join(testdir, "_testmain.go")
|
||||
err = ioutil.WriteFile(testmain, src, 0644)
|
||||
if err != nil {
|
||||
return bin, err
|
||||
}
|
||||
|
||||
// godog library may not be imported in tested package
|
||||
// but we need it for our testmain package.
|
||||
// So we look it up in available source paths
|
||||
// including vendor directory, supported since 1.5.
|
||||
try := []string{filepath.Join(abs, "vendor", godogImportPath)}
|
||||
for _, d := range build.Default.SrcDirs() {
|
||||
try = append(try, filepath.Join(d, godogImportPath))
|
||||
}
|
||||
godogPkg, err := locatePackage(try)
|
||||
if err != nil {
|
||||
return bin, err
|
||||
}
|
||||
|
||||
// make sure godog package archive is installed, gherkin
|
||||
// will be installed as dependency of godog
|
||||
cmd := exec.Command("go", "install", godogPkg.ImportPath)
|
||||
cmd.Env = os.Environ()
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, fmt.Errorf("failed to install godog package:\n%s", string(out))
|
||||
}
|
||||
|
||||
// collect all possible package dirs, will be
|
||||
// used for includes and linker
|
||||
pkgDirs := []string{workdir, testdir}
|
||||
for _, gopath := range gopaths {
|
||||
pkgDirs = append(pkgDirs, filepath.Join(gopath, "pkg", goos+"_"+goarch))
|
||||
}
|
||||
pkgDirs = uniqStringList(pkgDirs)
|
||||
|
||||
// compile godog testmain package archive
|
||||
// we do not depend on CGO so a lot of checks are not necessary
|
||||
testMainPkgOut := filepath.Join(testdir, "main.a")
|
||||
args := []string{
|
||||
"-o", testMainPkgOut,
|
||||
// "-trimpath", workdir,
|
||||
"-p", "main",
|
||||
"-complete",
|
||||
}
|
||||
// if godog library is in vendor directory
|
||||
// link it with import map
|
||||
if i := strings.LastIndex(godogPkg.ImportPath, "vendor/"); i != -1 {
|
||||
args = append(args, "-importmap", godogImportPath+"="+godogPkg.ImportPath)
|
||||
}
|
||||
for _, inc := range pkgDirs {
|
||||
args = append(args, "-I", inc)
|
||||
}
|
||||
args = append(args, "-pack", testmain)
|
||||
cmd = exec.Command(compiler, args...)
|
||||
cmd.Env = os.Environ()
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, fmt.Errorf("failed to compile testmain package:\n%s", string(out))
|
||||
}
|
||||
|
||||
// link test suite executable
|
||||
args = []string{
|
||||
"-o", bin,
|
||||
"-extld", build.Default.Compiler,
|
||||
"-buildmode=exe",
|
||||
}
|
||||
for _, link := range pkgDirs {
|
||||
args = append(args, "-L", link)
|
||||
}
|
||||
args = append(args, testMainPkgOut)
|
||||
cmd = exec.Command(linker, args...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
out, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return bin, fmt.Errorf("failed to link test executable:\n%s", string(out))
|
||||
}
|
||||
|
||||
return bin, nil
|
||||
}
|
||||
|
||||
func locatePackage(try []string) (*build.Package, error) {
|
||||
for _, path := range try {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
pkg, err := build.ImportDir(abs, 0)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return pkg, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to find godog package in any of:\n%s", strings.Join(try, "\n"))
|
||||
}
|
||||
|
||||
func uniqStringList(strs []string) (unique []string) {
|
||||
uniq := make(map[string]void, len(strs))
|
||||
for _, s := range strs {
|
||||
if _, ok := uniq[s]; !ok {
|
||||
uniq[s] = void{}
|
||||
unique = append(unique, s)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// buildTestMain if given package is valid
|
||||
// it scans test files for contexts
|
||||
// and produces a testmain source code.
|
||||
func buildTestMain(pkg *build.Package) ([]byte, bool, error) {
|
||||
var contexts []string
|
||||
var importPath string
|
||||
if nil != pkg {
|
||||
ctxs, err := processPackageTestFiles(
|
||||
pkg.TestGoFiles,
|
||||
pkg.XTestGoFiles,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
contexts = ctxs
|
||||
importPath = pkg.ImportPath
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Name string
|
||||
Contexts []string
|
||||
ImportPath string
|
||||
}{pkg.Name, contexts, importPath}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := runnerTemplate.Execute(&buf, data); err != nil {
|
||||
return nil, len(contexts) > 0, err
|
||||
}
|
||||
return buf.Bytes(), len(contexts) > 0, 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) {
|
||||
// file pack and looks for godog suite contexts to register
|
||||
// on run
|
||||
func processPackageTestFiles(packs ...[]string) ([]string, error) {
|
||||
var ctxs []string
|
||||
fset := token.NewFileSet()
|
||||
for _, pack := range packs {
|
||||
|
@ -122,52 +272,19 @@ func processPackageTestFiles(destDir string, packs ...[]string) ([]string, error
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
var failed []string
|
||||
for _, ctx := range ctxs {
|
||||
runes := []rune(ctx)
|
||||
if unicode.IsLower(runes[0]) {
|
||||
expected := append([]rune{unicode.ToUpper(runes[0])}, runes[1:]...)
|
||||
failed = append(failed, fmt.Sprintf("%s - should be: %s", ctx, string(expected)))
|
||||
}
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
return ctxs, fmt.Errorf("godog contexts must be exported:\n\t%s", strings.Join(failed, "\n\t"))
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,11 +5,9 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/godog"
|
||||
)
|
||||
|
@ -23,27 +21,10 @@ var stderr = statusOutputFilter(os.Stderr)
|
|||
func buildAndRun() (int, error) {
|
||||
var status int
|
||||
|
||||
dir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano())
|
||||
err := godog.Build(dir)
|
||||
bin, err := godog.Build()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
bin := filepath.Join(wd, "godog.test")
|
||||
|
||||
cmdb := exec.Command("go", "test", "-c", "-o", bin)
|
||||
cmdb.Dir = dir
|
||||
cmdb.Env = os.Environ()
|
||||
if details, err := cmdb.CombinedOutput(); err != nil {
|
||||
fmt.Fprintln(stderr, string(details))
|
||||
return 1, err
|
||||
}
|
||||
defer os.Remove(bin)
|
||||
|
||||
cmd := exec.Command(bin, os.Args[1:]...)
|
||||
|
|
|
@ -67,7 +67,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err er
|
|||
return
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
api := &apiFeature{}
|
||||
|
||||
s.BeforeScenario(api.resetResponse)
|
||||
|
|
|
@ -20,6 +20,6 @@ Feature: get version
|
|||
And the response should match json:
|
||||
"""
|
||||
{
|
||||
"version": "v0.4.3"
|
||||
"version": "v0.5.0"
|
||||
}
|
||||
"""
|
||||
|
|
|
@ -8,7 +8,6 @@ import (
|
|||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"github.com/DATA-DOG/go-txdb"
|
||||
"github.com/DATA-DOG/godog"
|
||||
"github.com/DATA-DOG/godog/gherkin"
|
||||
)
|
||||
|
@ -118,7 +117,7 @@ func (a *apiFeature) thereAreUsers(users *gherkin.DataTable) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
api := &apiFeature{}
|
||||
|
||||
s.BeforeScenario(api.resetResponse)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* file: examples/godogs/godog.go */
|
||||
package main
|
||||
|
||||
// Godogs to eat
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* file: examples/godogs/godog_test.go */
|
||||
package main
|
||||
|
||||
import (
|
||||
|
@ -26,7 +27,7 @@ func thereShouldBeRemaining(remaining int) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
|
||||
s.Step(`^I eat (\d+)$`, iEat)
|
||||
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
|
||||
|
|
|
@ -28,7 +28,7 @@ Feature: undefined step snippets
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendRequestTo)
|
||||
s.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe)
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ Feature: undefined step snippets
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendRequestToWith)
|
||||
s.Step(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeaderShouldBe)
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ Feature: undefined step snippets
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
func featureContext(s *godog.Suite) {
|
||||
func FeatureContext(s *godog.Suite) {
|
||||
s.Step(`^I pull from github\.com$`, iPullFromGithubcom)
|
||||
s.Step(`^the project should be there$`, theProjectShouldBeThere)
|
||||
}
|
||||
|
|
2
fmt.go
2
fmt.go
|
@ -30,7 +30,7 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH
|
|||
return godog.ErrPending
|
||||
}
|
||||
|
||||
{{end}}func featureContext(s *godog.Suite) { {{ range . }}
|
||||
{{end}}func FeatureContext(s *godog.Suite) { {{ range . }}
|
||||
s.Step({{ backticked .Expr }}, {{ .Method }}){{end}}
|
||||
}
|
||||
`))
|
||||
|
|
12
godog.go
12
godog.go
|
@ -6,13 +6,9 @@ Godog does not intervene with the standard "go test" command and it's 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 leverages
|
||||
a TestMain function 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 and replaces TestMain func if it was used in tests.
|
||||
Godog uses standard go ast and build utils to generate test suite package,
|
||||
compiles it with go test -c command. It accepts all your environment exported
|
||||
build related vars.
|
||||
Godog acts similar compared to go test command. It uses go
|
||||
compiler and linker tool in order to produce test executable. Godog
|
||||
contexts needs to be exported same as Test functions for go test.
|
||||
|
||||
For example, imagine you’re about to create the famous UNIX ls command.
|
||||
Before you begin, you describe how the feature should work, see the example below..
|
||||
|
@ -46,4 +42,4 @@ Godog was inspired by Behat and Cucumber the above description is taken from it'
|
|||
package godog
|
||||
|
||||
// Version of package - based on Semantic Versioning 2.0.0 http://semver.org/
|
||||
const Version = "v0.4.3"
|
||||
const Version = "v0.5.0"
|
||||
|
|
Двоичные данные
screenshots/passed.png
Двоичные данные
screenshots/passed.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 54 КиБ После Ширина: | Высота: | Размер: 80 КиБ |
Двоичные данные
screenshots/undefined.png
Двоичные данные
screenshots/undefined.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 109 КиБ После Ширина: | Высота: | Размер: 105 КиБ |
|
@ -2,6 +2,7 @@ package godog
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -215,8 +216,12 @@ func (s *suiteContext) iShouldHaveNumFeatureFiles(num int, files *gherkin.DocStr
|
|||
return fmt.Errorf("expected %d feature paths to be parsed, but have %d", len(expected), len(actual))
|
||||
}
|
||||
for i := 0; i < len(expected); i++ {
|
||||
if expected[i] != actual[i] {
|
||||
return fmt.Errorf(`expected feature path "%s" at position: %d, does not match actual "%s"`, expected[i], i, actual[i])
|
||||
split := strings.Split(expected[i], "/")
|
||||
exp := filepath.Join(split...)
|
||||
split = strings.Split(actual[i], "/")
|
||||
act := filepath.Join(split...)
|
||||
if exp != act {
|
||||
return fmt.Errorf(`expected feature path "%s" at position: %d, does not match actual "%s"`, exp, i, act)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
|
3
utils.go
3
utils.go
|
@ -6,6 +6,9 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
// empty struct value takes no space allocation
|
||||
type void struct{}
|
||||
|
||||
// a color code type
|
||||
type color int
|
||||
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче