Merge pull request #40 from DATA-DOG/build-tools

Revisit godog suite compilation and build tools to support vendoring
Этот коммит содержится в:
Gediminas Morkevicius 2016-06-15 22:15:33 +03:00 коммит произвёл GitHub
родитель 2162380725 58c203d2e7
коммит 5a17900dba
19 изменённых файлов: 311 добавлений и 474 удалений

Просмотреть файл

@ -1,6 +1,5 @@
language: go language: go
go: go:
- 1.4
- 1.5 - 1.5
- 1.6 - 1.6
- tip - tip

Просмотреть файл

@ -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** application while maintaining all test related source code in **_test.go**
files. files.
**Godog** acts similar compared to **go test** command. It uses **Godog** acts similar compared to **go test** command. It uses go
a **TestMain** hook introduced in `go1.4` and clones the package sources compiler and linker tool in order to produce test executable. Godog
to a temporary build directory. The only change it does is adding a runner contexts needs to be exported same as Test functions for go test.
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** ships gherkin parser dependency as a subpackage. This will **Godog** ships gherkin parser dependency as a subpackage. This will
ensure that it is always compatible with the installed version of godog. 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 #### 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 ``` go
/* file: examples/godogs/godog.go */ /* file: examples/godogs/godog.go */
@ -125,7 +121,8 @@ func main() { /* usual main func */ }
#### Step 4 #### 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 ``` go
/* file: examples/godogs/godog_test.go */ /* file: examples/godogs/godog_test.go */
@ -157,7 +154,7 @@ func thereShouldBeRemaining(remaining int) error {
return nil return nil
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
s.Step(`^I eat (\d+)$`, iEat) s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)
@ -193,6 +190,12 @@ See implementation examples:
### Changes ### 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** **2016-06-01**
- parse flags in main command, to show version and help without needing - parse flags in main command, to show version and help without needing
to compile test package and buildable go sources. to compile test package and buildable go sources.

108
ast.go
Просмотреть файл

@ -1,12 +1,6 @@
package godog package godog
import ( import "go/ast"
"go/ast"
"go/build"
"go/token"
"path"
"strings"
)
func astContexts(f *ast.File) []string { func astContexts(f *ast.File) []string {
var contexts []string var contexts []string
@ -35,103 +29,3 @@ func astContexts(f *ast.File) []string {
} }
return contexts 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)
}
}

Просмотреть файл

@ -1,162 +1,76 @@
package godog package godog
import ( import (
"bytes"
"go/format"
"go/parser" "go/parser"
"go/token" "go/token"
"strings"
"testing" "testing"
) )
var astMainFile = `package main var astContextSrc = `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 ( import (
"fmt" "github.com/DATA-DOG/godog"
"testing"
"os"
) )
func TestMain(m *testing.M) { func MyContext(s *godog.Suite) {
fmt.Println("hello")
os.Exit(0)
}` }`
var astPackAliases = `package main var astTwoContextSrc = `package lib
import ( import (
"testing" "github.com/DATA-DOG/godog"
a "fmt"
b "fmt"
) )
func TestMain(m *testing.M) { func ApiContext(s *godog.Suite) {
a.Println("a") }
b.Println("b")
func DBContext(s *godog.Suite) {
}` }`
var astAnonymousImport = `package main func astContextParse(src string, t *testing.T) []string {
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() fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", []byte(src), 0) f, err := parser.ParseFile(fset, "", []byte(src), 0)
if err != nil { if err != nil {
t.Fatalf("unexpected error while parsing ast: %v", err) t.Fatalf("unexpected error while parsing ast: %v", err)
} }
astDeleteTestMainFunc(f) return astContexts(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) { func TestShouldGetSingleContextFromSource(t *testing.T) {
actual := strings.TrimSpace(astProcess(astTestMainFile, t)) actual := astContextParse(astContextSrc, t)
expect := `package main` expect := []string{"MyContext"}
if actual != expect { if len(actual) != len(expect) {
t.Fatalf("expected output does not match: %s", actual) 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) { func TestShouldGetTwoContextsFromSource(t *testing.T) {
actual := strings.TrimSpace(astProcess(astPackAliases, t)) actual := astContextParse(astTwoContextSrc, t)
expect := `package main` expect := []string{"ApiContext", "DBContext"}
if actual != expect { if len(actual) != len(expect) {
t.Fatalf("expected output does not match: %s", actual) 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) { func TestShouldNotFindAnyContextsInEmptyFile(t *testing.T) {
actual := strings.TrimSpace(astProcess(astNormalFile, t)) actual := astContextParse(`package main`, t)
expect := astNormalFile
if actual != expect { if len(actual) != 0 {
t.Fatalf("expected output does not match: %s", actual) t.Fatalf("expected no contexts to be found, but there was some: %v", 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)
} }
} }

Просмотреть файл

@ -1,118 +1,268 @@
package godog package godog
import ( import (
"bytes"
"fmt"
"go/build" "go/build"
"go/format"
"go/parser" "go/parser"
"go/token" "go/token"
"io" "io/ioutil"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings"
"text/template" "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 ( import (
{{ if ne .Name "godog" }}"github.com/DATA-DOG/godog"{{ end }} "github.com/DATA-DOG/godog"
{{if .Contexts}}_test "{{.ImportPath}}"{{end}}
"os" "os"
"testing"
) )
const GodogSuiteName = "{{ .Name }}" func main() {
status := godog.Run(func (suite *godog.Suite) {
func TestMain(m *testing.M) {
status := {{ if ne .Name "godog" }}godog.{{ end }}Run(func (suite *{{ if ne .Name "godog" }}godog.{{ end }}Suite) {
{{range .Contexts}} {{range .Contexts}}
{{ . }}(suite) _test.{{ . }}(suite)
{{end}} {{end}}
}) })
os.Exit(status) os.Exit(status)
}`)) }`))
// Build scans clones current package into a temporary // Build creates a test package like go test command.
// godog suite test package. // 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 // If there are go test files, it first builds a test
// it removes it and all necessary unused imports related // package with standard go test command.
// to this function.
// //
// It also looks for any godog suite contexts and registers // Finally it generates godog suite executable which
// them in order to call them on execution. // registers exported godog contexts from the test files
// of tested package.
// //
// The test entry point which uses go1.4 TestMain func // Returns the path to generated executable
// is generated from the template above. func Build() (string, error) {
func Build(dir string) error { abs, err := filepath.Abs(".")
pkg, err := build.ImportDir(".", 0)
if err != nil { if err != nil {
return err return "", err
} }
return buildTestPackage(pkg, dir) bin := filepath.Join(abs, "godog.test")
// suffix with .exe for windows
if goos == "windows" {
bin += ".exe"
}
// 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 bin, err
}
workdir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano())
testdir := workdir
// 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))
}
// 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())
// 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
}
}
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
} }
// buildTestPackage clones a package and adds a godog func locatePackage(try []string) (*build.Package, error) {
// entry point with TestMain func in order to for _, path := range try {
// run the test suite. If TestMain func is found in tested abs, err := filepath.Abs(path)
// 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 { if err != nil {
return err 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"))
}
contexts, err := processPackageTestFiles( func uniqStringList(strs []string) (unique []string) {
dir, 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.TestGoFiles,
pkg.XTestGoFiles, pkg.XTestGoFiles,
) )
if err != nil { if err != nil {
return err return nil, false, err
} }
contexts = ctxs
// build godog runner test file importPath = pkg.ImportPath
out, err := os.Create(filepath.Join(dir, "godog_runner_test.go"))
if err != nil {
return err
} }
defer out.Close()
data := struct { data := struct {
Name string Name string
Contexts []string Contexts []string
}{pkg.Name, contexts} ImportPath string
}{pkg.Name, contexts, importPath}
return runnerTemplate.Execute(out, data) var buf bytes.Buffer
} if err := runnerTemplate.Execute(&buf, data); err != nil {
return nil, len(contexts) > 0, err
// 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
} }
} return buf.Bytes(), len(contexts) > 0, nil
}
return nil
} }
// processPackageTestFiles runs through ast of each test // processPackageTestFiles runs through ast of each test
// file pack and removes TestMain func if found. it also // file pack and looks for godog suite contexts to register
// looks for godog suite contexts to register on run // on run
func processPackageTestFiles(destDir string, packs ...[]string) ([]string, error) { func processPackageTestFiles(packs ...[]string) ([]string, error) {
var ctxs []string var ctxs []string
fset := token.NewFileSet() fset := token.NewFileSet()
for _, pack := range packs { for _, pack := range packs {
@ -122,52 +272,19 @@ func processPackageTestFiles(destDir string, packs ...[]string) ([]string, error
return ctxs, err return ctxs, err
} }
astDeleteTestMainFunc(node)
ctxs = append(ctxs, astContexts(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 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" "io"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"syscall" "syscall"
"time"
"github.com/DATA-DOG/godog" "github.com/DATA-DOG/godog"
) )
@ -23,27 +21,10 @@ var stderr = statusOutputFilter(os.Stderr)
func buildAndRun() (int, error) { func buildAndRun() (int, error) {
var status int var status int
dir := fmt.Sprintf(filepath.Join("%s", "godog-%d"), os.TempDir(), time.Now().UnixNano()) bin, err := godog.Build()
err := godog.Build(dir)
if err != nil { if err != nil {
return 1, err 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) defer os.Remove(bin)
cmd := exec.Command(bin, os.Args[1:]...) cmd := exec.Command(bin, os.Args[1:]...)

Просмотреть файл

@ -67,7 +67,7 @@ func (a *apiFeature) theResponseShouldMatchJSON(body *gherkin.DocString) (err er
return return
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
api := &apiFeature{} api := &apiFeature{}
s.BeforeScenario(api.resetResponse) s.BeforeScenario(api.resetResponse)

Просмотреть файл

@ -20,6 +20,6 @@ Feature: get version
And the response should match json: And the response should match json:
""" """
{ {
"version": "v0.4.3" "version": "v0.5.0"
} }
""" """

Просмотреть файл

@ -8,7 +8,6 @@ import (
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"github.com/DATA-DOG/go-txdb"
"github.com/DATA-DOG/godog" "github.com/DATA-DOG/godog"
"github.com/DATA-DOG/godog/gherkin" "github.com/DATA-DOG/godog/gherkin"
) )
@ -118,7 +117,7 @@ func (a *apiFeature) thereAreUsers(users *gherkin.DataTable) error {
return nil return nil
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
api := &apiFeature{} api := &apiFeature{}
s.BeforeScenario(api.resetResponse) s.BeforeScenario(api.resetResponse)

Просмотреть файл

@ -1,3 +1,4 @@
/* file: examples/godogs/godog.go */
package main package main
// Godogs to eat // Godogs to eat

Просмотреть файл

@ -1,3 +1,4 @@
/* file: examples/godogs/godog_test.go */
package main package main
import ( import (
@ -26,7 +27,7 @@ func thereShouldBeRemaining(remaining int) error {
return nil return nil
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
s.Step(`^there are (\d+) godogs$`, thereAreGodogs) s.Step(`^there are (\d+) godogs$`, thereAreGodogs)
s.Step(`^I eat (\d+)$`, iEat) s.Step(`^I eat (\d+)$`, iEat)
s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining) s.Step(`^there should be (\d+) remaining$`, thereShouldBeRemaining)

Просмотреть файл

@ -28,7 +28,7 @@ Feature: undefined step snippets
return godog.ErrPending return godog.ErrPending
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendRequestTo) s.Step(`^I send "([^"]*)" request to "([^"]*)"$`, iSendRequestTo)
s.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe) s.Step(`^the response code should be (\d+)$`, theResponseCodeShouldBe)
} }
@ -56,7 +56,7 @@ Feature: undefined step snippets
return godog.ErrPending return godog.ErrPending
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
s.Step(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendRequestToWith) s.Step(`^I send "([^"]*)" request to "([^"]*)" with:$`, iSendRequestToWith)
s.Step(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeaderShouldBe) s.Step(`^the response code should be (\d+) and header "([^"]*)" should be "([^"]*)"$`, theResponseCodeShouldBeAndHeaderShouldBe)
} }
@ -87,7 +87,7 @@ Feature: undefined step snippets
return godog.ErrPending return godog.ErrPending
} }
func featureContext(s *godog.Suite) { func FeatureContext(s *godog.Suite) {
s.Step(`^I pull from github\.com$`, iPullFromGithubcom) s.Step(`^I pull from github\.com$`, iPullFromGithubcom)
s.Step(`^the project should be there$`, theProjectShouldBeThere) s.Step(`^the project should be there$`, theProjectShouldBeThere)
} }

2
fmt.go
Просмотреть файл

@ -30,7 +30,7 @@ var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetH
return godog.ErrPending return godog.ErrPending
} }
{{end}}func featureContext(s *godog.Suite) { {{ range . }} {{end}}func FeatureContext(s *godog.Suite) { {{ range . }}
s.Step({{ backticked .Expr }}, {{ .Method }}){{end}} s.Step({{ backticked .Expr }}, {{ .Method }}){{end}}
} }
`)) `))

Просмотреть файл

@ -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 You can leverage both frameworks to functionally test your application while
maintaining all test related source code in *_test.go files. maintaining all test related source code in *_test.go files.
Godog acts similar compared to go test command. It leverages Godog acts similar compared to go test command. It uses go
a TestMain function introduced in go1.4 and clones the package sources compiler and linker tool in order to produce test executable. Godog
to a temporary build directory. The only change it does is adding a runner contexts needs to be exported same as Test functions for go test.
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.
For example, imagine youre about to create the famous UNIX ls command. For example, imagine youre about to create the famous UNIX ls command.
Before you begin, you describe how the feature should work, see the example below.. 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 package godog
// Version of package - based on Semantic Versioning 2.0.0 http://semver.org/ // 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

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 54 КиБ

После

Ширина:  |  Высота:  |  Размер: 80 КиБ

Двоичные данные
screenshots/undefined.png

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 109 КиБ

После

Ширина:  |  Высота:  |  Размер: 105 КиБ

Просмотреть файл

@ -2,6 +2,7 @@ package godog
import ( import (
"fmt" "fmt"
"path/filepath"
"strconv" "strconv"
"strings" "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)) return fmt.Errorf("expected %d feature paths to be parsed, but have %d", len(expected), len(actual))
} }
for i := 0; i < len(expected); i++ { for i := 0; i < len(expected); i++ {
if expected[i] != actual[i] { split := strings.Split(expected[i], "/")
return fmt.Errorf(`expected feature path "%s" at position: %d, does not match actual "%s"`, expected[i], i, actual[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 return nil

Просмотреть файл

@ -6,6 +6,9 @@ import (
"strings" "strings"
) )
// empty struct value takes no space allocation
type void struct{}
// a color code type // a color code type
type color int type color int