diff --git a/.gitignore b/.gitignore index f6b9e24..7037299 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /cmd/godog/godog +/example/example diff --git a/README.md b/README.md index dd3a33b..4bffbee 100644 --- a/README.md +++ b/README.md @@ -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]. +### Install + + go install github.com/DATA-DOG/godog/cmd/godog + ### Be aware that 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 -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_gherkin]: http://godoc.org/github.com/DATA-DOG/godog/gherkin "Documentation on godoc for gherkin" diff --git a/arguments.go b/arguments.go index afa11b1..1f7b18f 100644 --- a/arguments.go +++ b/arguments.go @@ -8,14 +8,18 @@ import ( ) // 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 { value interface{} } // StepArgument func creates a step argument. // used in cases when calling another step from -// within a StepHandlerFunc +// within a StepHandler function. func StepArgument(value interface{}) *Arg { return &Arg{value: value} } diff --git a/builder.go b/builder.go index d04f4bd..4294c75 100644 --- a/builder.go +++ b/builder.go @@ -22,8 +22,8 @@ type builder struct { tpl *template.Template } -func newBuilder() *builder { - return &builder{ +func newBuilder(buildPath string) (*builder, error) { + b := &builder{ files: make(map[string]*ast.File), fset: token.NewFileSet(), tpl: template.Must(template.New("main").Parse(`package main @@ -39,6 +39,18 @@ func main() { 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 { @@ -129,24 +141,18 @@ func (b *builder) merge() (*ast.File, error) { return ast.MergePackageFiles(pkg, ast.FilterImportDuplicates), nil } -// Build creates a runnable godog executable file -// from current package source and test files -// it merges the files with the help of go/ast into +// Build creates a runnable Godog executable file +// from current package source and test source files. +// +// The package files are merged with the help of go/ast into // a single main package file which has a custom -// main function to run 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) { - b := 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 - }) + b, err := newBuilder(".") if err != nil { return nil, err } diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..23df247 --- /dev/null +++ b/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 + diff --git a/example/ls.feature b/example/ls.feature new file mode 100644 index 0000000..59780dc --- /dev/null +++ b/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 + """ diff --git a/example/ls.go b/example/ls.go new file mode 100644 index 0000000..72ee3c5 --- /dev/null +++ b/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 + }) +} diff --git a/example/ls_test.go b/example/ls_test.go new file mode 100644 index 0000000..f38ae8f --- /dev/null +++ b/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) +} diff --git a/fmt.go b/fmt.go index 10c045f..8d6a0bf 100644 --- a/fmt.go +++ b/fmt.go @@ -7,7 +7,12 @@ import ( ) // 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 { Node(interface{}) Failed(*gherkin.Step, *StepDef, error) diff --git a/fmt_pretty.go b/fmt_pretty.go index d7acd17..cc5c847 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -22,11 +22,9 @@ var outlinePlaceholderRegexp *regexp.Regexp = regexp.MustCompile("<[^>]+>") // a built in default pretty formatter type pretty struct { - feature *gherkin.Feature - commentPos int - doneBackground bool - background *gherkin.Background - scenario *gherkin.Scenario + feature *gherkin.Feature + commentPos int + backgroundSteps int // outline outlineExamples int @@ -56,21 +54,17 @@ func (f *pretty) Node(node interface{}) { fmt.Println("") } f.feature = t - f.scenario = nil - f.background = nil f.features = append(f.features, t) + // print feature header fmt.Println(bcl(t.Token.Keyword+": ", white) + t.Title) fmt.Println(t.Description) - case *gherkin.Background: - // do not repeat background for the same feature - if f.background == nil && f.scenario == nil { - f.background = t - f.commentPos = longestStep(t.Steps, t.Token.Length()) - // print background node - fmt.Println("\n" + s(t.Token.Indent) + bcl(t.Token.Keyword+":", white)) + // print background header + if t.Background != nil { + f.commentPos = longestStep(t.Background.Steps, t.Background.Token.Length()) + f.backgroundSteps = len(t.Background.Steps) + fmt.Println("\n" + s(t.Background.Token.Indent) + bcl(t.Background.Token.Keyword+":", white)) } case *gherkin.Scenario: - f.scenario = t f.commentPos = longestStep(t.Steps, t.Token.Length()) if t.Outline != nil { f.outlineSteps = []interface{}{} // reset steps list @@ -162,10 +156,10 @@ func (f *pretty) Summary() { fmt.Println(elapsed) } -func (f *pretty) printOutlineExample() { +func (f *pretty) printOutlineExample(scenario *gherkin.Scenario) { var failed error clr := green - tbl := f.scenario.Outline.Examples + tbl := scenario.Outline.Examples firstExample := f.outlineExamples == len(tbl.Rows)-1 for i, act := range f.outlineSteps { @@ -187,7 +181,7 @@ func (f *pretty) printOutlineExample() { if firstExample { // in first example, we need to print steps var text string - ostep := f.scenario.Outline.Steps[i] + ostep := scenario.Outline.Steps[i] if def != nil { if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 { var pos int @@ -216,7 +210,7 @@ func (f *pretty) printOutlineExample() { max := longest(tbl) // an example table header if firstExample { - out := f.scenario.Outline + out := scenario.Outline fmt.Println("") fmt.Println(s(out.Token.Indent) + bcl(out.Token.Keyword+":", white)) row := tbl.Rows[0] @@ -308,15 +302,18 @@ func (f *pretty) printStepKind(stepAction interface{}) { step, def, c, err = f.stepDetails(stepAction) // do not print background more than once - if f.scenario == nil && step.Background != f.background { + switch { + case step.Background != nil && f.backgroundSteps == 0: return + case step.Background != nil && f.backgroundSteps > 0: + f.backgroundSteps -= 1 } if f.outlineExamples != 0 { f.outlineSteps = append(f.outlineSteps, stepAction) if len(f.outlineSteps) == f.outlineNumSteps { // an outline example steps has went through - f.printOutlineExample() + f.printOutlineExample(step.Scenario) f.outlineExamples -= 1 } return // wait till example steps @@ -385,7 +382,7 @@ func longestStep(steps []*gherkin.Step, base int) int { ret := base for _, step := range steps { length := step.Token.Length() - if length > base { + if length > ret { ret = length } } diff --git a/gherkin/example/example b/gherkin/example/example deleted file mode 100755 index 4f9748e..0000000 Binary files a/gherkin/example/example and /dev/null differ diff --git a/gherkin/example/ls.feature b/gherkin/example/ls.feature deleted file mode 100644 index 931d436..0000000 --- a/gherkin/example/ls.feature +++ /dev/null @@ -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 - """ diff --git a/gherkin/example/main.go b/gherkin/example/main.go deleted file mode 100644 index 8ae400d..0000000 --- a/gherkin/example/main.go +++ /dev/null @@ -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") -} diff --git a/suite.go b/suite.go index ed8c5ff..ea55eb8 100644 --- a/suite.go +++ b/suite.go @@ -16,26 +16,30 @@ import ( // it can be either a string or a *regexp.Regexp type Regexp interface{} -// StepHandler is a function contract for -// step handler +// StepHandler is a func to handle the step // -// It receives all arguments which -// will be matched according to the regular expression +// The handler receives all arguments which +// will be matched according to the Regexp // which is passed with a step registration. -// The error in return - represents a reason of failure. // -// Returning signals that the step has finished -// and that the feature runner can move on to the next -// step. +// The error in return - represents a reason of failure. +// All consequent scenario steps are skipped. +// +// Returning signals that the step has finished and that +// the feature runner can move on to the next step. type StepHandler func(...*Arg) error // ErrUndefined is returned in case if step definition was not found var ErrUndefined = fmt.Errorf("step is undefined") // StepDef is a registered step definition -// contains a StepHandler, a regexp which -// is used to match a step and Args which -// were matched by last step +// contains a StepHandler and regexp which +// is used to match a step. Args which +// 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 { Args []*Arg Handler StepHandler @@ -43,7 +47,16 @@ type StepDef struct { } // 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 { Step(expr Regexp, h StepHandler) // suite events @@ -80,17 +93,16 @@ func New() *suite { // Step allows to register a StepHandler in Godog // 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 -// or handler does not satisfy StepHandler interface // // Note that if there are two handlers which may match // 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 -// step error will be raised. +// If none of the StepHandlers are matched, then +// ErrUndefined error will be returned. func (s *suite) Step(expr Regexp, h StepHandler) { var regex *regexp.Regexp @@ -112,13 +124,20 @@ func (s *suite) Step(expr Regexp, h StepHandler) { } // 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()) { s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, f) } // 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)) { s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, f) } @@ -131,6 +150,13 @@ func (s *suite) BeforeStep(f func(*gherkin.Step)) { // AfterStep registers an function or method // 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)) { s.afterStepHandlers = append(s.afterStepHandlers, f) } @@ -147,7 +173,7 @@ func (s *suite) AfterSuite(f func()) { s.afterSuiteHandlers = append(s.afterSuiteHandlers, f) } -// Run - runs a godog feature suite +// Run starts the Godog feature suite func (s *suite) Run() { var err error if !flag.Parsed() { diff --git a/utils.go b/utils.go index 04f2f60..95c76ce 100644 --- a/utils.go +++ b/utils.go @@ -6,10 +6,12 @@ import ( "strings" ) +// a color code type type color int const ansiEscape = "\x1b" +// some ansi colors const ( black color = iota + 30 red