Этот коммит содержится в:
gedi 2015-06-23 13:51:45 +03:00
родитель 6e65757f89
коммит e824fde3f9
15 изменённых файлов: 244 добавлений и 100 удалений

1
.gitignore предоставленный
Просмотреть файл

@ -1 +1,2 @@
/cmd/godog/godog /cmd/godog/godog
/example/example

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

@ -9,6 +9,10 @@ describe a feature of your application and how it should work, and only then imp
The project is inspired by [behat][behat] and [cucumber][cucumber] and is based on cucumber [gherkin specification][gherkin]. The project is inspired by [behat][behat] and [cucumber][cucumber] and is based on cucumber [gherkin specification][gherkin].
### Install
go install github.com/DATA-DOG/godog/cmd/godog
### Be aware that ### Be aware that
The work is still in progress and is not functional yet, neither it is intended for production usage. The work is still in progress and is not functional yet, neither it is intended for production usage.
@ -21,7 +25,9 @@ See **.travis.yml** for supported **go** versions.
### License ### License
Licensed under the [three clause BSD license][license] All package dependencies are **MIT** or **BSD** licensed.
**Godog** is licensed under the [three clause BSD license][license]
[godoc]: http://godoc.org/github.com/DATA-DOG/godog "Documentation on godoc" [godoc]: http://godoc.org/github.com/DATA-DOG/godog "Documentation on godoc"
[godoc_gherkin]: http://godoc.org/github.com/DATA-DOG/godog/gherkin "Documentation on godoc for gherkin" [godoc_gherkin]: http://godoc.org/github.com/DATA-DOG/godog/gherkin "Documentation on godoc for gherkin"

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

@ -8,14 +8,18 @@ import (
) )
// Arg is an argument for StepHandler parsed from // Arg is an argument for StepHandler parsed from
// the regexp submatch to handle the step // the regexp submatch to handle the step.
//
// In future versions, it may be replaced with
// an argument injection toolkit using reflect
// package.
type Arg struct { type Arg struct {
value interface{} value interface{}
} }
// StepArgument func creates a step argument. // StepArgument func creates a step argument.
// used in cases when calling another step from // used in cases when calling another step from
// within a StepHandlerFunc // within a StepHandler function.
func StepArgument(value interface{}) *Arg { func StepArgument(value interface{}) *Arg {
return &Arg{value: value} return &Arg{value: value}
} }

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

@ -22,8 +22,8 @@ type builder struct {
tpl *template.Template tpl *template.Template
} }
func newBuilder() *builder { func newBuilder(buildPath string) (*builder, error) {
return &builder{ b := &builder{
files: make(map[string]*ast.File), files: make(map[string]*ast.File),
fset: token.NewFileSet(), fset: token.NewFileSet(),
tpl: template.Must(template.New("main").Parse(`package main tpl: template.Must(template.New("main").Parse(`package main
@ -39,6 +39,18 @@ func main() {
suite.Run() suite.Run()
}`)), }`)),
} }
return b, 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") {
if err := b.parseFile(path); err != nil {
return err
}
}
return err
})
} }
func (b *builder) parseFile(path string) error { func (b *builder) parseFile(path string) error {
@ -129,24 +141,18 @@ func (b *builder) merge() (*ast.File, error) {
return ast.MergePackageFiles(pkg, ast.FilterImportDuplicates), nil return ast.MergePackageFiles(pkg, ast.FilterImportDuplicates), nil
} }
// Build creates a runnable godog executable file // Build creates a runnable Godog executable file
// from current package source and test files // from current package source and test source files.
// it merges the files with the help of go/ast into //
// The package files are merged with the help of go/ast into
// a single main package file which has a custom // a single main package file which has a custom
// main function to run features // 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) { func Build() ([]byte, error) {
b := newBuilder() b, err := newBuilder(".")
err := filepath.Walk(".", func(path string, file os.FileInfo, err error) error {
if file.IsDir() && file.Name() != "." {
return filepath.SkipDir
}
if err == nil && strings.HasSuffix(path, ".go") {
if err := b.parseFile(path); err != nil {
return err
}
}
return err
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

7
example/README.md Обычный файл
Просмотреть файл

@ -0,0 +1,7 @@
# ls feature
In order to test our **ls** feature with **Godog**, run:
go install github.com/DATA-DOG/godog/cmd/godog
$GOPATH/bin/godog ls.feature

27
example/ls.feature Обычный файл
Просмотреть файл

@ -0,0 +1,27 @@
Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list directory contents
Background:
Given I am in a directory "test"
Scenario: lists files in directory
Given I have a file named "foo"
And I have a file named "bar"
When I run ls
Then I should get output:
"""
bar
foo
"""
Scenario: lists files and directories
Given I have a file named "foo"
And I have a directory named "dir"
When I run ls
Then I should get output:
"""
dir
foo
"""

34
example/ls.go Обычный файл
Просмотреть файл

@ -0,0 +1,34 @@
package main
import (
"io"
"log"
"os"
"path/filepath"
)
func main() {
var location string
switch {
case os.Args[1] != "":
location = os.Args[1]
default:
location = "."
}
if err := ls(location, os.Stdout); err != nil {
log.Fatal(err)
}
}
func ls(path string, w io.Writer) error {
return filepath.Walk(path, func(p string, f os.FileInfo, err error) error {
switch {
case f.IsDir() && f.Name() != "." && f.Name() != ".." && filepath.Base(path) != f.Name():
w.Write([]byte(f.Name() + "\n"))
return filepath.SkipDir
case !f.IsDir():
w.Write([]byte(f.Name() + "\n"))
}
return err
})
}

65
example/ls_test.go Обычный файл
Просмотреть файл

@ -0,0 +1,65 @@
package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/DATA-DOG/godog"
)
type lsFeature struct {
dir string
buf *bytes.Buffer
}
func lsFeatureContext(s godog.Suite) {
c := &lsFeature{buf: bytes.NewBuffer(make([]byte, 1024))}
s.Step(`^I am in a directory "([^"]*)"$`, c.iAmInDirectory)
s.Step(`^I have a (file|directory) named "([^"]*)"$`, c.iHaveFileOrDirectoryNamed)
s.Step(`^I run ls$`, c.iRunLs)
s.Step(`^I should get output:$`, c.iShouldGetOutput)
}
func (f *lsFeature) iAmInDirectory(args ...*godog.Arg) error {
f.dir = os.TempDir() + "/" + args[0].String()
if err := os.RemoveAll(f.dir); err != nil && !os.IsNotExist(err) {
return err
}
return os.Mkdir(f.dir, 0775)
}
func (f *lsFeature) iHaveFileOrDirectoryNamed(args ...*godog.Arg) (err error) {
if len(f.dir) == 0 {
return fmt.Errorf("the directory was not chosen yet")
}
switch args[0].String() {
case "file":
err = ioutil.WriteFile(f.dir+"/"+args[1].String(), []byte{}, 0664)
case "directory":
err = os.Mkdir(f.dir+"/"+args[1].String(), 0775)
}
return err
}
func (f *lsFeature) iShouldGetOutput(args ...*godog.Arg) error {
expected := args[0].PyString().Lines
actual := strings.Split(strings.TrimSpace(f.buf.String()), "\n")
if len(expected) != len(actual) {
return fmt.Errorf("number of expected output lines %d, does not match actual: %d", len(expected), len(actual))
}
for i, line := range actual {
if line != expected[i] {
return fmt.Errorf(`expected line "%s" at position: %d to match "%s", but it did not`, expected[i], i, line)
}
}
return nil
}
func (f *lsFeature) iRunLs(args ...*godog.Arg) error {
f.buf.Reset()
return ls(f.dir, f.buf)
}

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

@ -7,7 +7,12 @@ import (
) )
// Formatter is an interface for feature runner // Formatter is an interface for feature runner
// output summary presentation // output summary presentation.
//
// New formatters may be created to represent
// suite results in different ways. These new
// formatters needs to be registered with a
// RegisterFormatter function call
type Formatter interface { type Formatter interface {
Node(interface{}) Node(interface{})
Failed(*gherkin.Step, *StepDef, error) Failed(*gherkin.Step, *StepDef, error)

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

@ -24,9 +24,7 @@ var outlinePlaceholderRegexp *regexp.Regexp = regexp.MustCompile("<[^>]+>")
type pretty struct { type pretty struct {
feature *gherkin.Feature feature *gherkin.Feature
commentPos int commentPos int
doneBackground bool backgroundSteps int
background *gherkin.Background
scenario *gherkin.Scenario
// outline // outline
outlineExamples int outlineExamples int
@ -56,21 +54,17 @@ func (f *pretty) Node(node interface{}) {
fmt.Println("") fmt.Println("")
} }
f.feature = t f.feature = t
f.scenario = nil
f.background = nil
f.features = append(f.features, t) f.features = append(f.features, t)
// print feature header
fmt.Println(bcl(t.Token.Keyword+": ", white) + t.Title) fmt.Println(bcl(t.Token.Keyword+": ", white) + t.Title)
fmt.Println(t.Description) fmt.Println(t.Description)
case *gherkin.Background: // print background header
// do not repeat background for the same feature if t.Background != nil {
if f.background == nil && f.scenario == nil { f.commentPos = longestStep(t.Background.Steps, t.Background.Token.Length())
f.background = t f.backgroundSteps = len(t.Background.Steps)
f.commentPos = longestStep(t.Steps, t.Token.Length()) fmt.Println("\n" + s(t.Background.Token.Indent) + bcl(t.Background.Token.Keyword+":", white))
// print background node
fmt.Println("\n" + s(t.Token.Indent) + bcl(t.Token.Keyword+":", white))
} }
case *gherkin.Scenario: case *gherkin.Scenario:
f.scenario = t
f.commentPos = longestStep(t.Steps, t.Token.Length()) f.commentPos = longestStep(t.Steps, t.Token.Length())
if t.Outline != nil { if t.Outline != nil {
f.outlineSteps = []interface{}{} // reset steps list f.outlineSteps = []interface{}{} // reset steps list
@ -162,10 +156,10 @@ func (f *pretty) Summary() {
fmt.Println(elapsed) fmt.Println(elapsed)
} }
func (f *pretty) printOutlineExample() { func (f *pretty) printOutlineExample(scenario *gherkin.Scenario) {
var failed error var failed error
clr := green clr := green
tbl := f.scenario.Outline.Examples tbl := scenario.Outline.Examples
firstExample := f.outlineExamples == len(tbl.Rows)-1 firstExample := f.outlineExamples == len(tbl.Rows)-1
for i, act := range f.outlineSteps { for i, act := range f.outlineSteps {
@ -187,7 +181,7 @@ func (f *pretty) printOutlineExample() {
if firstExample { if firstExample {
// in first example, we need to print steps // in first example, we need to print steps
var text string var text string
ostep := f.scenario.Outline.Steps[i] ostep := scenario.Outline.Steps[i]
if def != nil { if def != nil {
if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 { if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 {
var pos int var pos int
@ -216,7 +210,7 @@ func (f *pretty) printOutlineExample() {
max := longest(tbl) max := longest(tbl)
// an example table header // an example table header
if firstExample { if firstExample {
out := f.scenario.Outline out := scenario.Outline
fmt.Println("") fmt.Println("")
fmt.Println(s(out.Token.Indent) + bcl(out.Token.Keyword+":", white)) fmt.Println(s(out.Token.Indent) + bcl(out.Token.Keyword+":", white))
row := tbl.Rows[0] row := tbl.Rows[0]
@ -308,15 +302,18 @@ func (f *pretty) printStepKind(stepAction interface{}) {
step, def, c, err = f.stepDetails(stepAction) step, def, c, err = f.stepDetails(stepAction)
// do not print background more than once // do not print background more than once
if f.scenario == nil && step.Background != f.background { switch {
case step.Background != nil && f.backgroundSteps == 0:
return return
case step.Background != nil && f.backgroundSteps > 0:
f.backgroundSteps -= 1
} }
if f.outlineExamples != 0 { if f.outlineExamples != 0 {
f.outlineSteps = append(f.outlineSteps, stepAction) f.outlineSteps = append(f.outlineSteps, stepAction)
if len(f.outlineSteps) == f.outlineNumSteps { if len(f.outlineSteps) == f.outlineNumSteps {
// an outline example steps has went through // an outline example steps has went through
f.printOutlineExample() f.printOutlineExample(step.Scenario)
f.outlineExamples -= 1 f.outlineExamples -= 1
} }
return // wait till example steps return // wait till example steps
@ -385,7 +382,7 @@ func longestStep(steps []*gherkin.Step, base int) int {
ret := base ret := base
for _, step := range steps { for _, step := range steps {
length := step.Token.Length() length := step.Token.Length()
if length > base { if length > ret {
ret = length ret = length
} }
} }

Двоичные данные
gherkin/example/example

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

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

@ -1,15 +0,0 @@
Feature: ls
In order to see the directory structure
As a UNIX user
I need to be able to list the current directory's contents
Scenario:
Given I am in a directory "test"
And I have a file named "foo"
And I have a file named "bar"
When I run "ls"
Then I should get:
"""
bar
foo
"""

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

@ -1,21 +0,0 @@
package main
import (
"log"
"os"
"github.com/DATA-DOG/godog/gherkin"
)
func main() {
feature, err := gherkin.ParseFile("ls.feature")
switch {
case err == gherkin.ErrEmpty:
log.Println("the feature file is empty and does not describe any feature")
return
case err != nil:
log.Println("the feature file is incorrect or could not be read:", err)
os.Exit(1)
}
log.Println("have parsed a feature:", feature.Title, "with", len(feature.Scenarios), "scenarios")
}

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

@ -16,26 +16,30 @@ import (
// it can be either a string or a *regexp.Regexp // it can be either a string or a *regexp.Regexp
type Regexp interface{} type Regexp interface{}
// StepHandler is a function contract for // StepHandler is a func to handle the step
// step handler
// //
// It receives all arguments which // The handler receives all arguments which
// will be matched according to the regular expression // will be matched according to the Regexp
// which is passed with a step registration. // which is passed with a step registration.
// The error in return - represents a reason of failure.
// //
// Returning signals that the step has finished // The error in return - represents a reason of failure.
// and that the feature runner can move on to the next // All consequent scenario steps are skipped.
// step. //
// Returning signals that the step has finished and that
// the feature runner can move on to the next step.
type StepHandler func(...*Arg) error type StepHandler func(...*Arg) error
// ErrUndefined is returned in case if step definition was not found // ErrUndefined is returned in case if step definition was not found
var ErrUndefined = fmt.Errorf("step is undefined") var ErrUndefined = fmt.Errorf("step is undefined")
// StepDef is a registered step definition // StepDef is a registered step definition
// contains a StepHandler, a regexp which // contains a StepHandler and regexp which
// is used to match a step and Args which // is used to match a step. Args which
// were matched by last step // were matched by last executed step
//
// This structure is passed to the formatter
// when step is matched and is either failed
// or successful
type StepDef struct { type StepDef struct {
Args []*Arg Args []*Arg
Handler StepHandler Handler StepHandler
@ -43,7 +47,16 @@ type StepDef struct {
} }
// Suite is an interface which allows various contexts // Suite is an interface which allows various contexts
// to register step definitions and event handlers // to register steps and event handlers.
//
// When running a test suite, this interface is passed
// to all functions (contexts), which have it as a
// first and only argument.
//
// Note that all event hooks does not catch panic errors
// in order to have a trace information. Only step
// executions are catching panic error since it may
// be a context specific error.
type Suite interface { type Suite interface {
Step(expr Regexp, h StepHandler) Step(expr Regexp, h StepHandler)
// suite events // suite events
@ -80,17 +93,16 @@ func New() *suite {
// Step allows to register a StepHandler in Godog // Step allows to register a StepHandler in Godog
// feature suite, the handler will be applied to all // feature suite, the handler will be applied to all
// steps matching the given regexp expr // steps matching the given Regexp expr
// //
// It will panic if expr is not a valid regular expression // It will panic if expr is not a valid regular expression
// or handler does not satisfy StepHandler interface
// //
// Note that if there are two handlers which may match // Note that if there are two handlers which may match
// the same step, then the only first matched handler // the same step, then the only first matched handler
// will be applied // will be applied.
// //
// If none of the StepHandlers are matched, then a pending // If none of the StepHandlers are matched, then
// step error will be raised. // ErrUndefined error will be returned.
func (s *suite) Step(expr Regexp, h StepHandler) { func (s *suite) Step(expr Regexp, h StepHandler) {
var regex *regexp.Regexp var regex *regexp.Regexp
@ -112,13 +124,20 @@ func (s *suite) Step(expr Regexp, h StepHandler) {
} }
// BeforeSuite registers a function or method // BeforeSuite registers a function or method
// to be run once before suite runner // to be run once before suite runner.
//
// Use it to prepare the test suite for a spin.
// Connect and prepare database for instance...
func (s *suite) BeforeSuite(f func()) { func (s *suite) BeforeSuite(f func()) {
s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f) s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f)
} }
// BeforeScenario registers a function or method // BeforeScenario registers a function or method
// to be run before every scenario // to be run before every scenario.
//
// It is a good practice to restore the default state
// before every scenario so it would be isolated from
// any kind of state.
func (s *suite) BeforeScenario(f func(*gherkin.Scenario)) { func (s *suite) BeforeScenario(f func(*gherkin.Scenario)) {
s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f) s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f)
} }
@ -131,6 +150,13 @@ func (s *suite) BeforeStep(f func(*gherkin.Step)) {
// AfterStep registers an function or method // AfterStep registers an function or method
// to be run after every scenario // to be run after every scenario
//
// It may be convenient to return a different kind of error
// in order to print more state details which may help
// in case of step failure
//
// In some cases, for example when running a headless
// browser, to take a screenshot after failure.
func (s *suite) AfterStep(f func(*gherkin.Step, error)) { func (s *suite) AfterStep(f func(*gherkin.Step, error)) {
s.afterStepHandlers = append(s.afterStepHandlers, f) s.afterStepHandlers = append(s.afterStepHandlers, f)
} }
@ -147,7 +173,7 @@ func (s *suite) AfterSuite(f func()) {
s.afterSuiteHandlers = append(s.afterSuiteHandlers, f) s.afterSuiteHandlers = append(s.afterSuiteHandlers, f)
} }
// Run - runs a godog feature suite // Run starts the Godog feature suite
func (s *suite) Run() { func (s *suite) Run() {
var err error var err error
if !flag.Parsed() { if !flag.Parsed() {

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

@ -6,10 +6,12 @@ import (
"strings" "strings"
) )
// a color code type
type color int type color int
const ansiEscape = "\x1b" const ansiEscape = "\x1b"
// some ansi colors
const ( const (
black color = iota + 30 black color = iota + 30
red red