pax_global_header00006660000000000000000000000064127420415620014515gustar00rootroot0000000000000052 comment=71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/000077500000000000000000000000001274204156200207115ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/.travis.yml000066400000000000000000000001211274204156200230140ustar00rootroot00000000000000language: go sudo: false go: - 1.4 install: go get -v -t ./... script: make gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/LICENSE.txt000077500000000000000000000020371274204156200225410ustar00rootroot00000000000000Copyright (c) 2015 Loren Segal Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/Makefile000066400000000000000000000001611274204156200223470ustar00rootroot00000000000000default: test deps: go get ./... build: deps go install ./cmd/gucumber test: build go test ./... gucumber gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/README.md000077500000000000000000000053201274204156200221730ustar00rootroot00000000000000# Gucumber [![GoDoc](http://img.shields.io/badge/godoc-reference-blue.svg)](http://godoc.org/github.com/gucumber/gucumber) [![Build Status](https://img.shields.io/travis/gucumber/gucumber.svg)](https://travis-ci.org/gucumber/gucumber) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/gucumber/gucumber/blob/master/LICENSE.txt) An implementation of [Cucumber][cuke] BDD-style testing for Go. # Installing ```sh $ go get github.com/gucumber/gucumber/cmd/gucumber ``` # Usage Cucumber tests are made up of plain text ".feature" files and program source "step definitions", which for Gucumber are written in Go. ## Features Put [feature files][features] `internal/features/` with whatever organization you prefer. For example, you might create `internal/features/accounts/login.feature` with the following text: ``` @login Feature: Login Support Scenario: User successfully logs in Given I have user/pass "foo" / "bar" And they log into the website with user "foo" and password "bar" Then the user should be successfully logged in ``` ## Step Definitions Create step definitions to match each step in your feature files. These go in ".go" files in the same `internal/features/` directory. We might create `internal/features/accounts/step_definitions.go`: ```go package accounts import ( . "github.com/gucumber/gucumber" ) func init() { user, pass := "", "" Before("@login", func() { // runs before every feature or scenario tagged with @login generatePassword() }) Given(`^I have user/pass "(.+?)" / "(.+?)"$`, func(u, p string) { user, pass = u, p }) // ... Then(`^the user should be successfully logged in$`, func() { if !userIsLoggedIn() { T.Fail("user should have been logged in") } }) } ``` ### T? The `T` value is a [testing.T](http://golang.org/pkg/testing/#T) style value that represents the test context for each test. It mostly supports `Errorf(fmt, args...)`, but also supports other convenience methods. See the [API documentation](http://godoc.org/github.com/gucumber/gucumber#TestingT) for more information. ## Running To run your tests, execute: ```sh $ gucumber ``` You can also specify the path to features in command line arguments: ```sh $ gucumber path/to/features ``` You can also filter features and scenarios by tags: ```sh $ gucumber -tags=@login # only run login feature(s) ``` Or: ```sh $ gucumber -tags=~@slow # ignore all "slow" scenarios ``` # Copyright This library was written by [Loren Segal][lsegal] in 2015. It is licensed for use under the [MIT license][mit]. [cuke]: http://cukes.info [features]: https://github.com/cucumber/cucumber/wiki/Feature-Introduction [lsegal]: http://gnuu.org [mit]: http://opensource.org/licenses/MIT gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/builder.go000066400000000000000000000076171274204156200227010ustar00rootroot00000000000000package gucumber import ( "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "strings" "text/template" ) const ( importMarkerFile = "importmarker__.go" testFile = "gucumbertest__.go" ) // BuildAndRunDir builds the given director's features into Go Code. // Using the filters provided. An error is returned if the build fails. func BuildAndRunDir(dir string, filters []string) error { return buildAndRunDir(dir, filters, "") } // BuildAndRunDirWithGoBuildTags builds the given director's features into Go // Code using the filters provided. Also takes a string for the build tags to // be passed to the go command. An error is returned if the build fails. // // If goBuildTags is empty, the param will be ignored. func BuildAndRunDirWithGoBuildTags(dir string, filters []string, goBuildTags string) error { return buildAndRunDir(dir, filters, goBuildTags) } func buildAndRunDir(dir string, filters []string, goBuildTags string) error { defer buildCleanup(dir) info := buildInfo{ Imports: []string{}, FeaturesPath: fmt.Sprintf("%q", dir), Filters: filters, } goFiles, _ := filepath.Glob(filepath.Join(dir, "*.go")) goFiles2, _ := filepath.Glob(filepath.Join(dir, "**", "*.go")) goFiles = append(goFiles, goFiles2...) // write special constants to packages so they can be imported for _, file := range goFiles { ifile := filepath.Join(filepath.Dir(file), importMarkerFile) if _, err := os.Stat(ifile); err != nil { pkgName := filepath.Base(filepath.Dir(file)) if pkgName == "_test" { continue } fullPkg := assembleImportPath(file) if fullPkg == "" { return fmt.Errorf("could not determine package path for %s", file) } info.Imports = append(info.Imports, fullPkg) src := fmt.Sprintf("package %s\nvar IMPORT_MARKER = true\n", pkgName) err = ioutil.WriteFile(ifile, []byte(src), 0664) if err != nil { return err } } } // write main test stub os.MkdirAll(filepath.Join(dir, "_test"), 0777) f, err := os.Create(filepath.Join(dir, "_test", testFile)) if err != nil { return err } tplMain.Execute(f, info) f.Close() // now run the command tfile := "./" + filepath.ToSlash(dir) + "/_test/" + testFile var cmd *exec.Cmd if len(goBuildTags) > 0 { cmd = exec.Command("go", "run", "-tags", goBuildTags, tfile) } else { cmd = exec.Command("go", "run", tfile) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { buildCleanup(dir) os.Exit(1) } return nil } // ToSlash is being used to coerce the different // os PathSeparators into the forward slash // as the forward slash is required by Go's import statement func assembleImportPath(file string) string { a, _ := filepath.Abs(filepath.Dir(file)) absPath, fullPkg := filepath.ToSlash(a), "" for _, p := range filepath.SplitList(os.Getenv("GOPATH")) { a, _ = filepath.Abs(p) p = filepath.ToSlash(a) if strings.HasPrefix(absPath, p) { prefixPath := filepath.ToSlash(filepath.Join(p, "src")) rpath, _ := filepath.Rel(prefixPath, absPath) fullPkg = filepath.ToSlash(rpath) break } } return fullPkg } type buildInfo struct { Imports []string FeaturesPath string Filters []string } func buildCleanup(dir string) { g, _ := filepath.Glob(filepath.Join(dir, importMarkerFile)) g2, _ := filepath.Glob(filepath.Join(dir, "**", importMarkerFile)) g = append(g, g2...) for _, d := range g { os.Remove(d) } p := filepath.Join(dir, "_test") if _, err := os.Stat(p); err == nil { os.RemoveAll(p) } } var tplMain = template.Must(template.New("main").Parse(` package main import ( "github.com/gucumber/gucumber" {{range $n, $i := .Imports}}_i{{$n}} "{{$i}}" {{end}} ) var ( {{range $n, $i := .Imports}}_ci{{$n}} = _i{{$n}}.IMPORT_MARKER {{end}} ) func main() { {{if .Filters}} gucumber.GlobalContext.Filters = []string{ {{range $_, $f := .Filters}}"{{$f}}", {{end}} } {{end}} gucumber.GlobalContext.RunDir({{.FeaturesPath}}) } `)) gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/builder_test.go000066400000000000000000000006401274204156200237250ustar00rootroot00000000000000package gucumber import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" ) func TestPackageImportPathIsExtractedFromFilePath(t *testing.T) { //arrange packageName := "github.com/username/reponame" file := filepath.Join(os.Getenv("GOPATH"), "src/github.com/username/reponame/main.go") //act importPath := assembleImportPath(file) //assert assert.Equal(t, packageName, importPath) } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/cmd/000077500000000000000000000000001274204156200214545ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/cmd/gucumber/000077500000000000000000000000001274204156200232655ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/cmd/gucumber/gucumber.go000077500000000000000000000001311274204156200254230ustar00rootroot00000000000000package main import "github.com/gucumber/gucumber" func main() { gucumber.RunMain() } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/000077500000000000000000000000001274204156200223405ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/i18n.go000077500000000000000000000024771274204156200234630ustar00rootroot00000000000000package gherkin type Language string // Translation represents the Gherkin syntax keywords in a single language. type Translation struct { // Language specific term representing a feature. Feature string // Language specific term representing the feature background. Background string // Language specific term representing a scenario. Scenario string // Language specific term representing a scenario outline. Outline string // Language specific term representing the "And" step. And string // Language specific term representing the "Given" step. Given string // Language specific term representing the "When" step. When string // Language specific term representing the "Then" step. Then string // Language specific term representing a scenario outline prefix. Examples string } const ( // The language code for English translations. LANG_EN = Language("en") ) var ( // Translations contains internationalized translations of the Gherkin // syntax keywords in a variety of supported languages. Translations = map[Language]Translation{ LANG_EN: Translation{ Feature: "Feature", Background: "Background", Scenario: "Scenario", Outline: "Scenario Outline", And: "And", Given: "Given", When: "When", Then: "Then", Examples: "Examples", }, } ) gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/parser.go000066400000000000000000000162621274204156200241720ustar00rootroot00000000000000package gherkin import ( "fmt" "path/filepath" "regexp" "strings" ) var reNL = regexp.MustCompile(`\r?\n`) func ParseFilename(data, filename string) ([]Feature, error) { lines := reNL.Split(data, -1) p := parser{ lines: lines, features: []Feature{}, translations: Translations[LANG_EN], filename: filename, } if err := p.parse(); err != nil { return nil, err } if len(p.features) == 0 { return p.features, p.err("no features parsed") } return p.features, nil } func Parse(data string) ([]Feature, error) { return ParseFilename(data, "") } type parser struct { translations Translation features []Feature lines []string lineNo int lastLine int started bool filename string } func (p parser) err(msg string, args ...interface{}) error { l := p.lineNo + 1 if l > len(p.lines) { l = len(p.lines) } if p.filename == "" { p.filename = ".feature" } return fmt.Errorf("parse error (%s:%d): %s.", filepath.Base(p.filename), l, fmt.Sprintf(msg, args...)) } func (p *parser) line() string { if p.lineNo < len(p.lines) { return p.lines[p.lineNo] } return "" } func (p *parser) nextLine() bool { p.lastLine = p.lineNo if p.started { p.lineNo++ } p.started = true for ; p.lineNo < len(p.lines); p.lineNo++ { line, _ := p.lineStripped() if line != "" && !strings.HasPrefix(line, "#") { break } } return p.stillReading() } func (p *parser) stillReading() bool { return p.lineNo < len(p.lines) } func (p *parser) unread() { if p.lineNo == 0 { p.started = false } p.lineNo = p.lastLine } func (p *parser) parse() error { for p.stillReading() { if err := p.consumeFeature(); err != nil { return err } } return nil } func (p *parser) lineStripped() (string, int) { line := p.line() for i := 0; i < len(line); i++ { c := line[i : i+1] if c != " " && c != "\t" { return line[i:], i } } return line, 0 } func (p *parser) consumeFeature() error { desc := []string{} tags, err := p.consumeTags() if err != nil { return err } if !p.nextLine() { if len(tags) > 0 { return p.err("tags not applied to feature") } return nil } line, startIndent := p.lineStripped() parts := strings.SplitN(line, " ", 2) prefix := p.translations.Feature + ":" title := "" if parts[0] != prefix { return p.err("expected %q, found %q", prefix, line) } if len(parts) > 1 { title = parts[1] } f := Feature{ Title: title, Tags: tags, Scenarios: []Scenario{}, Filename: p.filename, Line: p.lineNo, } var stags *[]string seenScenario, seenBackground := false, false for p.stillReading() { // consume until we get to Background or Scenario // find indentation of next line if p.nextLine() { _, indent := p.lineStripped() p.unread() if indent <= startIndent { // de-dented break // done parsing this feature } } prevLine := p.lineNo tags, err = p.consumeTags() if err != nil { return err } if p.lineNo != prevLine { // tags found stags = &tags } if !p.nextLine() { break } line, _ := p.lineStripped() parts := strings.SplitN(line, ":", 2) switch parts[0] { case p.translations.Background: if seenScenario { return p.err("illegal background after scenario") } else if seenBackground { return p.err("multiple backgrounds not allowed") } seenBackground = true b := Scenario{Filename: p.filename, Line: p.lineNo} err = p.consumeScenario(&b) if err != nil { return err } if stags != nil { b.Tags = *stags } else { b.Tags = []string{} } f.Background = b stags = nil case p.translations.Scenario, p.translations.Outline: seenScenario = true s := Scenario{Filename: p.filename, Line: p.lineNo} err = p.consumeScenario(&s) if err != nil { return err } if stags != nil { s.Tags = *stags } else { s.Tags = []string{} } if len(parts) > 1 { s.Title = strings.TrimSpace(parts[1]) } f.Scenarios = append(f.Scenarios, s) stags = nil default: // then this must be a description if stags != nil { return p.err("illegal description text after tags") } else if seenScenario || seenBackground { return p.err("illegal description text after scenario") } desc = append(desc, line) } } f.Description = strings.Join(desc, "\n") p.features = append(p.features, f) return nil } func (p *parser) consumeScenario(scenario *Scenario) error { scenario.Steps = []Step{} _, startIndent := p.lineStripped() for p.nextLine() { // consume all steps _, indent := p.lineStripped() if indent <= startIndent { // de-dented p.unread() break } if err := p.consumeStep(scenario); err != nil { return err } } return nil } func (p *parser) consumeStep(scenario *Scenario) error { line, indent := p.lineStripped() parts := strings.SplitN(line, " ", 2) switch parts[0] { case p.translations.Given, p.translations.Then, p.translations.When, p.translations.And: var arg StringData if len(parts) < 2 { return p.err("expected step text after %q", parts[0]) } if p.nextLine() { l, _ := p.lineStripped() p.unread() if len(l) > 0 && (l[0] == '|' || l == `"""`) { // this is step argument data arg = p.consumeIndentedData(indent) } } var stype StepType switch parts[0] { case p.translations.Given: stype = StepType("Given") case p.translations.When: stype = StepType("When") case p.translations.Then: stype = StepType("Then") case p.translations.And: stype = StepType("And") } s := Step{ Filename: p.filename, Line: p.lineNo, Type: stype, Text: parts[1], Argument: arg, } scenario.Steps = append(scenario.Steps, s) case p.translations.Examples + ":": scenario.Examples = p.consumeIndentedData(indent) default: return p.err("illegal step prefix %q", parts[0]) } return nil } func (p *parser) consumeIndentedData(scenarioIndent int) StringData { stringData := []string{} startIndent, quoted := -1, false for p.nextLine() { var line string var indent int if startIndent == -1 { // first line line, indent = p.lineStripped() startIndent = indent if line == `"""` { quoted = true // this is a docstring data block continue // ignore this from data } } else { line = p.line() if len(line) <= startIndent { // not enough text, not part of indented data p.unread() break } if line[0:startIndent] != strings.Repeat(" ", startIndent) { // not enough indentation, not part of indented data p.unread() break } line = line[startIndent:] if !quoted && line[0] != '|' { // tabular data must start with | on each line p.unread() break } if quoted && line == `"""` { // end quote on docstring block break } } stringData = append(stringData, line) } return StringData(strings.Join(stringData, "\n")) } func (p *parser) consumeTags() ([]string, error) { tags := []string{} if !p.nextLine() { return tags, nil } line, _ := p.lineStripped() if len(p.lines) == 0 || !strings.HasPrefix(line, "@") { p.unread() return tags, nil } for _, t := range strings.Split(line, " ") { if t == "" { continue } if t[0:1] != "@" { return nil, p.err("invalid tag %q", t) } tags = append(tags, t) } return tags, nil } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/parser_test.go000066400000000000000000000206621274204156200252300ustar00rootroot00000000000000package gherkin import ( "testing" "github.com/stretchr/testify/assert" ) func TestParseSingleScenario(t *testing.T) { s := ` # a line comment @tag1 @tag2 Feature: Refund item # a comment embedded in the description A multiline description of the Feature that can contain any text like Rules: - a - b @tag1 @tag2 Scenario: Jeff returns a faulty microwave Given Jeff has bought a microwave for $100 And he has a receipt When he returns the microwave Then Jeff should be refunded $100 ` features, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 1, len(features)) assert.Equal(t, "Refund item", features[0].Title) assert.Equal(t, "@tag1", features[0].Tags[0]) assert.Equal(t, "@tag2", features[0].Tags[1]) assert.Equal(t, 1, len(features[0].Scenarios)) assert.Equal(t, "Jeff returns a faulty microwave", features[0].Scenarios[0].Title) assert.Equal(t, "A multiline description of the Feature\nthat can contain any text like\nRules:\n- a\n- b", features[0].Description) assert.Equal(t, 4, len(features[0].Scenarios[0].Steps)) assert.Equal(t, "@tag1", features[0].Scenarios[0].Tags[0]) assert.Equal(t, "@tag2", features[0].Scenarios[0].Tags[1]) assert.Equal(t, StepType("Given"), features[0].Scenarios[0].Steps[0].Type) assert.Equal(t, "Jeff has bought a microwave for $100", features[0].Scenarios[0].Steps[0].Text) assert.Equal(t, StepType("And"), features[0].Scenarios[0].Steps[1].Type) assert.Equal(t, "he has a receipt", features[0].Scenarios[0].Steps[1].Text) assert.Equal(t, StepType("When"), features[0].Scenarios[0].Steps[2].Type) assert.Equal(t, "he returns the microwave", features[0].Scenarios[0].Steps[2].Text) assert.Equal(t, StepType("Then"), features[0].Scenarios[0].Steps[3].Type) assert.Equal(t, "Jeff should be refunded $100", features[0].Scenarios[0].Steps[3].Text) } func TestMultipleScenarios(t *testing.T) { s := ` Feature: Parsing multiple scenarios Scenario: Scenario name here Given some precondition Then something happens Scenario: Another scenario Given another precondition Then something happens ` features, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 2, len(features[0].Scenarios)) assert.Equal(t, 2, len(features[0].Scenarios[0].Steps)) assert.Equal(t, 2, len(features[0].Scenarios[1].Steps)) } func TestBackgroundAndScenarios(t *testing.T) { s := ` Feature: Parsing multiple scenarios @tag_on_background Background: Given there is some background Scenario: Scenario name here Given some precondition Then something happens Scenario: Another scenario Given another precondition Then something happens ` features, err := Parse(s) assert.NoError(t, err) assert.Equal(t, "@tag_on_background", features[0].Background.Tags[0]) assert.Equal(t, "there is some background", features[0].Background.Steps[0].Text) assert.Equal(t, 2, len(features[0].Scenarios)) } func TestMultipleFeatures(t *testing.T) { s := ` Feature: Feature 1 Scenario: Scenario name here Given some precondition Then something happens Feature: Feature 2 Scenario: Another scenario Given another precondition Then something happens ` f, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 2, len(f)) assert.Equal(t, "Feature 1", f[0].Title) assert.Equal(t, "Feature 2", f[1].Title) assert.Equal(t, 1, len(f[0].Scenarios)) assert.Equal(t, 1, len(f[1].Scenarios)) } func TestTagParsing(t *testing.T) { f, err := Parse("@tag1 @tag2@tag3\nFeature: Tag parsing") assert.NoError(t, err) assert.Equal(t, 2, len(f[0].Tags)) assert.Equal(t, "@tag1", f[0].Tags[0]) assert.Equal(t, "@tag2@tag3", f[0].Tags[1]) } func TestBacktrackingCommentsAtEnd(t *testing.T) { s := ` Feature: Comments at end Scenario: Scenario name here Given some precondition Then something happens # comments here ` _, err := Parse(s) assert.NoError(t, err) } func TestBacktrackingCommentsDontAffectIndent(t *testing.T) { s := ` Feature: Comments at end Scenario: Scenario name here Given some precondition Then something happens # comments here Scenario: Another scenario Given a step ` f, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 2, len(f[0].Scenarios)) } func TestScenarioOutlines(t *testing.T) { s := ` Feature: Scenario outlines Scenario Outline: Scenario 1 Given some value Then some result Examples: | foo | bar | | 1 | 2 | | 3 | 4 | Scenario: Scenario 2 Given some other scenario ` f, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 2, len(f[0].Scenarios)) assert.Equal(t, StringData("| foo | bar |\n| 1 | 2 |\n| 3 | 4 |"), f[0].Scenarios[0].Examples) } func TestStepArguments(t *testing.T) { s := ` Feature: Step arguments Scenario: Scenario 1 Given some data | 1 | 2 | | 3 | 4 | And some docstring """ hello world """ And some table | 1 | 2 | Then other text Scenario: Scenario 2 Given some other scenario ` f, err := Parse(s) assert.NoError(t, err) assert.Equal(t, 2, len(f[0].Scenarios)) assert.Equal(t, StringData("| 1 | 2 |\n| 3 | 4 |"), f[0].Scenarios[0].Steps[0].Argument) assert.Equal(t, StringData(" hello\n world"), f[0].Scenarios[0].Steps[1].Argument) assert.Equal(t, StringData("| 1 | 2 |"), f[0].Scenarios[0].Steps[2].Argument) assert.Equal(t, "other text", f[0].Scenarios[0].Steps[3].Text) } func TestFailureNoFeature(t *testing.T) { _, err := Parse("") assert.EqualError(t, err, `parse error (.feature:1): no features parsed.`) } func TestTagWithoutFeature(t *testing.T) { _, err := Parse("@tag") assert.EqualError(t, err, `parse error (.feature:1): tags not applied to feature.`) } func TestFailureExpectingFeature(t *testing.T) { _, err := Parse("@tag\n@tag") assert.EqualError(t, err, `parse error (.feature:2): expected "Feature:", found "@tag".`) } func TestFailureInvalidTag(t *testing.T) { _, err := Parse("@tag tag") assert.EqualError(t, err, `parse error (.feature:1): invalid tag "tag".`) } func TestFailureDescriptionAfterTags(t *testing.T) { s := ` Feature: Descriptions after tags @tag1 Descriptions should not be allowed after tags Scenario: Scenario name here Given some precondition Then something happens ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:4): illegal description text after tags.`) } func TestFailureDescriptionAfterScenario(t *testing.T) { s := ` Feature: Descriptions after scenario Scenario: Scenario name here Given some precondition Then something happens Descriptions should not be allowed after scenario Scenario: Another scenario Given some precondition Then something happens ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:7): illegal description text after scenario.`) } func TestFailureMultipleBackgrounds(t *testing.T) { s := ` Feature: Multiple backgrounds Background: Given one Background: Given two ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:6): multiple backgrounds not allowed.`) } func TestFailureBackgroundAfterScenario(t *testing.T) { s := ` Feature: Background after scenario Scenario: Scenario name here Given some precondition Then something happens Background: Given it's after a scenario ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:7): illegal background after scenario.`) } func TestFailureInvalidStep(t *testing.T) { s := ` Feature: Invalid steps Scenario: Scenario name here Invalid step ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:4): illegal step prefix "Invalid".`) } func TestFailureNoStepText(t *testing.T) { s := ` Feature: No step text Scenario: Scenario name here Given ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:4): expected step text after "Given".`) } func TestFailureInvalidTagOnScenario(t *testing.T) { s := ` Feature: Invalid tag on scenario @invalid tags Scenario: Given a scenario ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:3): invalid tag "tags".`) } func TestFailureInvalidBackground(t *testing.T) { s := ` Feature: Invalid background Background: Invalid step Scenario: A scenario Given a scenario ` _, err := Parse(s) assert.EqualError(t, err, `parse error (.feature:4): illegal step prefix "Invalid".`) } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/types.go000077500000000000000000000130171274204156200240400ustar00rootroot00000000000000package gherkin import ( "reflect" "strings" ) // Feature represents the top-most construct in a Gherkin document. A feature // contains one or more scenarios, which in turn contains multiple steps. type Feature struct { // The filename where the feature was defined Filename string // The line number where the feature was defined Line int // The feature's title. Title string // A longer description of the feature. This is not used during runtime. Description string // Any tags associated with this feature. Tags []string // Any background scenario data that is executed prior to scenarios. Background Scenario // The scenarios associated with this feature. Scenarios []Scenario // The longest line length in the feature (including title) longestLine int } // Scenario represents a scenario (or background) of a given feature. type Scenario struct { // The filename where the scenario was defined Filename string // The line number where the scenario was defined Line int // The scenario's title. For backgrounds, this is the empty string. Title string // Any tags associated with this scenario. Tags []string // All steps associated with the scenario. Steps []Step // Contains all scenario outline example data, if provided. Examples StringData // The longest line length in the scenario (including title) longestLine int } // Step represents an individual step making up a gucumber scenario. type Step struct { // The filename where the step was defined Filename string // The line number where the step was defined Line int // The step's "type" (Given, When, Then, And, ...) // // Note that this field is normalized to the English form (e.g., "Given"). Type StepType // The text contained in the step (minus the "Type" prefix). Text string // Argument represents multi-line argument data attached to a step. Argument StringData } // StringData is multi-line docstring text attached to a step. type StringData string // TabularData is tabular text data attached to a step. type TabularData [][]string // TabularDataMap is tabular text data attached to a step organized in map // form of the header name and its associated row data. type TabularDataMap map[string][]string // StepType represents a given step type. type StepType string // ToTable turns StringData type into a TabularData type func (s StringData) ToTable() TabularData { var tabData TabularData lines := strings.Split(string(s), "\n") for _, line := range lines { row := strings.Split(line, "|") row = row[1 : len(row)-1] for i, c := range row { row[i] = strings.TrimSpace(c) } tabData = append(tabData, row) } return tabData } // IsTabular returns whether the argument data is a table func (s StringData) IsTabular() bool { return len(s) > 0 && s[0] == '|' } // ToMap converts a regular table to a map of header names to their row data. // For example: // // t := TabularData{[]string{"header1", "header2"}, []string{"col1", "col2"}} // t.ToMap() // // Output: // // map[string][]string{ // // "header1": []string{"col1"}, // // "header2": []string{"col2"}, // // } func (t TabularData) ToMap() TabularDataMap { m := TabularDataMap{} if len(t) > 1 { for _, th := range t[0] { m[th] = []string{} } for _, tr := range t[1:] { for c, td := range tr { m[t[0][c]] = append(m[t[0][c]], td) } } } return m } // NumRows returns the number of rows in a table map func (t TabularDataMap) NumRows() int { if len(t) == 0 { return 0 } return len(t[reflect.ValueOf(t).MapKeys()[0].String()]) } // LongestLine returns the longest step line in a scenario. func (s *Scenario) LongestLine() int { if s.longestLine == 0 { s.longestLine = len("Scenario: " + s.Title) for _, step := range s.Steps { if l := len(string(step.Type) + " " + step.Text); l > s.longestLine { s.longestLine = l } } } return s.longestLine } // LongestLine returns the longest step line in a feature. func (f *Feature) LongestLine() int { if f.longestLine == 0 { f.longestLine = len("Feature: " + f.Title) for _, s := range f.Scenarios { if l := s.LongestLine(); l > f.longestLine { f.longestLine = l } } } return f.longestLine } // FilterMatched returns true if the set of input filters match the feature's tags. func (f *Feature) FilterMatched(filters ...string) bool { return matchTags(f.Tags, filters) } // FilterMatched returns true if the set of input filters match the feature's tags. func (s *Scenario) FilterMatched(f *Feature, filters ...string) bool { t := []string{} t = append(t, f.Tags...) t = append(t, s.Tags...) return matchTags(t, filters) } func matchTags(tags []string, filters []string) bool { if len(filters) == 0 { // no filters means everything passes return true } for _, f := range filters { if matchFilter(f, tags) { return true // if any filter matches we succeed } } return false } func matchFilter(filter string, tags []string) bool { parts := strings.Split(filter, ",") for _, part := range parts { // all parts must match part = strings.TrimSpace(part) if part == "" { continue } if part[0] == '~' { // filter has to NOT match any tags for _, t := range tags { if part[1:] == string(t) { // tag matched, this should not happen return false } } // nothing matched, we can continue on } else { result := false for _, t := range tags { if part == string(t) { // found a match in a tag result = true break } } if !result { // no matches, this filter failed return false } } } return true } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/gherkin/types_test.go000066400000000000000000000036711274204156200251010ustar00rootroot00000000000000package gherkin import ( "testing" "github.com/stretchr/testify/assert" ) func TestStringDataToTable(t *testing.T) { s := StringData("| a | b |\n| 1 | 2 |\n| 3 | 4 |") tab := TabularData{ []string{"a", "b"}, []string{"1", "2"}, []string{"3", "4"}, } assert.Equal(t, tab, s.ToTable()) } func TestTabularDataToMap(t *testing.T) { tab := TabularData{ []string{"a", "b", "c", "d"}, []string{"1", "2", "3", "4"}, []string{"5", "6", "7", "8"}, []string{"9", "A", "B", "C"}, } m := TabularDataMap{ "a": []string{"1", "5", "9"}, "b": []string{"2", "6", "A"}, "c": []string{"3", "7", "B"}, "d": []string{"4", "8", "C"}, } assert.Equal(t, m, tab.ToMap()) assert.Equal(t, 3, m.NumRows()) } func TestTabularDataMapEmpty(t *testing.T) { var tab TabularData var m TabularDataMap // only headers tab = TabularData{[]string{"a", "b", "c", "d"}} m = TabularDataMap{} assert.Equal(t, m, tab.ToMap()) assert.Equal(t, 0, m.NumRows()) // completely empty tab = TabularData{} m = TabularDataMap{} assert.Equal(t, m, tab.ToMap()) assert.Equal(t, 0, m.NumRows()) } func TestScenarioFilters(t *testing.T) { f := &Feature{Tags: []string{}} s := Scenario{Tags: []string{"@a", "@b"}} assert.True(t, s.FilterMatched(f)) assert.False(t, s.FilterMatched(f, "a")) assert.True(t, s.FilterMatched(f, "@a")) assert.True(t, s.FilterMatched(f, "@c", "@a")) assert.False(t, s.FilterMatched(f, "~@a")) assert.False(t, s.FilterMatched(f, "@a,@c")) assert.True(t, s.FilterMatched(f, "@a,@b", "@c")) s = Scenario{Tags: []string{}} assert.False(t, s.FilterMatched(f, "@a")) } func TestFeatureFilters(t *testing.T) { s := Feature{Tags: []string{"@a", "@b"}} assert.True(t, s.FilterMatched()) assert.False(t, s.FilterMatched("a")) assert.True(t, s.FilterMatched("@a")) assert.True(t, s.FilterMatched("@c", "@a")) assert.False(t, s.FilterMatched("~@a")) assert.False(t, s.FilterMatched("@a,@c")) assert.True(t, s.FilterMatched("@a,@b", "@c")) } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/internal/000077500000000000000000000000001274204156200225255ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/internal/features/000077500000000000000000000000001274204156200243435ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/internal/features/basic/000077500000000000000000000000001274204156200254245ustar00rootroot00000000000000gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/internal/features/basic/scenario.feature000077500000000000000000000010631274204156200306070ustar00rootroot00000000000000@scenario @fast Feature: Scenarios This package should support the use of scenarios. @basic Scenario: Basic usage Given I have an initial step And I have a second step When I run the "gucumber" command Then this scenario should execute 1 time and pass And setup was called 1 time @outline Scenario Outline: Scenario outline Given I perform + Then I should get And setup was called 1 time Examples: | val1 | val2 | result | | 1 | 2 | 3 | | 3 | 4 | 7 | gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/internal/features/basic/step_definitions.go000066400000000000000000000015711274204156200313250ustar00rootroot00000000000000package basic import ( . "github.com/gucumber/gucumber" "github.com/stretchr/testify/assert" ) func init() { executions := 100 result := 1 beforeAllCalls := 0 BeforeAll(func() { beforeAllCalls++ }) Before("@basic", func() { executions = 0 }) Given(`^I have an initial step$`, func() { assert.Equal(T, 1, 1) }) And(`^I have a second step$`, func() { assert.Equal(T, 2, 2) }) When(`^I run the "(.+?)" command$`, func(s1 string) { assert.Equal(T, "gucumber", s1) }) Then(`^this scenario should execute (\d+) time and pass$`, func(i1 int) { executions++ assert.Equal(T, i1, executions) }) Given(`^I perform (\d+) \+ (\d+)$`, func(i1 int, i2 int) { result = i1 + i2 }) Then(`^I should get (\d+)$`, func(i1 int) { assert.Equal(T, result, i1) }) Then(`^setup was called (\d+) times?$`, func(i2 int) { assert.Equal(T, i2, beforeAllCalls) }) } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/run_main.go000066400000000000000000000014371274204156200230550ustar00rootroot00000000000000package gucumber import ( "flag" "fmt" ) type filters []string func (f *filters) String() string { return fmt.Sprint(*f) } func (f *filters) Set(value string) error { *f = append(*f, value) return nil } var filterFlag filters var goBuildTags string func init() { flag.Var(&filterFlag, "tags", "comma-separated list of tags to filter scenarios by") flag.StringVar(&goBuildTags, "go-tags", "", "space seperated list of tags, wrap in quotes to specify multiple tags") } func RunMain() { flag.Parse() var dir string if flag.NArg() == 0 { dir = "internal/features" } else { dir = flag.Arg(0) } filt := []string{} for _, f := range filterFlag { filt = append(filt, string(f)) } if err := BuildAndRunDirWithGoBuildTags(dir, filt, goBuildTags); err != nil { panic(err) } } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/runner.go000077500000000000000000000213511274204156200225560ustar00rootroot00000000000000package gucumber import ( "bytes" "fmt" "io/ioutil" "os" "path/filepath" "regexp" "runtime" "strings" "github.com/gucumber/gucumber/gherkin" "github.com/shiena/ansicolor" ) const ( clrWhite = "0" clrRed = "31" clrGreen = "32" clrYellow = "33" clrCyan = "36" txtUnmatchInt = `(\d+)` txtUnmatchFloat = `(-?\d+(?:\.\d+)?)` txtUnmatchStr = `"(.+?)"` ) var ( reUnmatchInt = regexp.MustCompile(txtUnmatchInt) reUnmatchFloat = regexp.MustCompile(txtUnmatchFloat) reUnmatchStr = regexp.MustCompile(`(<|").+?("|>)`) reOutlineVal = regexp.MustCompile(`<(.+?)>`) ) type Runner struct { *Context Features []*gherkin.Feature Results []RunnerResult Unmatched []*gherkin.Step FailCount int SkipCount int } type RunnerResult struct { *TestingT *gherkin.Feature *gherkin.Scenario } func (c *Context) RunDir(dir string) (*Runner, error) { g, _ := filepath.Glob(filepath.Join(dir, "*.feature")) g2, _ := filepath.Glob(filepath.Join(dir, "**", "*.feature")) g = append(g, g2...) runner, err := c.RunFiles(g) if err != nil { panic(err) } if len(runner.Unmatched) > 0 { fmt.Println("Some steps were missing, you can add them by using the following step definition stubs: ") fmt.Println("") fmt.Print(runner.MissingMatcherStubs()) } os.Exit(runner.FailCount) return runner, err } func (c *Context) RunFiles(featureFiles []string) (*Runner, error) { r := Runner{ Context: c, Features: []*gherkin.Feature{}, Results: []RunnerResult{}, Unmatched: []*gherkin.Step{}, } for _, file := range featureFiles { fd, err := os.Open(file) if err != nil { return nil, err } defer fd.Close() b, err := ioutil.ReadAll(fd) if err != nil { return nil, err } fs, err := gherkin.ParseFilename(string(b), file) if err != nil { return nil, err } for _, f := range fs { r.Features = append(r.Features, &f) } } r.run() return &r, nil } func (c *Runner) MissingMatcherStubs() string { var buf bytes.Buffer matches := map[string]bool{} buf.WriteString(`import . "github.com/gucumber/gucumber"` + "\n\n") buf.WriteString("func init() {\n") for _, m := range c.Unmatched { numInts, numFloats, numStrs := 1, 1, 1 str, args := m.Text, []string{} str = reUnmatchInt.ReplaceAllStringFunc(str, func(s string) string { args = append(args, fmt.Sprintf("i%d int", numInts)) numInts++ return txtUnmatchInt }) str = reUnmatchFloat.ReplaceAllStringFunc(str, func(s string) string { args = append(args, fmt.Sprintf("s%d float64", numFloats)) numFloats++ return txtUnmatchFloat }) str = reUnmatchStr.ReplaceAllStringFunc(str, func(s string) string { args = append(args, fmt.Sprintf("s%d string", numStrs)) numStrs++ return txtUnmatchStr }) if len(m.Argument) > 0 { if m.Argument.IsTabular() { args = append(args, "table [][]string") } else { args = append(args, "data string") } } // Don't duplicate matchers. This is mostly for scenario outlines. if matches[str] { continue } matches[str] = true fmt.Fprintf(&buf, "\t%s(`^%s$`, func(%s) {\n\t\tT.Skip() // pending\n\t})\n\n", m.Type, str, strings.Join(args, ", ")) } buf.WriteString("}\n") return buf.String() } func (c *Runner) run() { if c.BeforeAllFilter != nil { c.BeforeAllFilter() } for _, f := range c.Features { c.runFeature(f) } if c.AfterAllFilter != nil { c.AfterAllFilter() } c.line("0;1", "Finished (%d passed, %d failed, %d skipped).\n", len(c.Results)-c.FailCount-c.SkipCount, c.FailCount, c.SkipCount) } func (c *Runner) runFeature(f *gherkin.Feature) { if !f.FilterMatched(c.Filters...) { result := false // if any scenarios match, we will run those for _, s := range f.Scenarios { if s.FilterMatched(f, c.Filters...) { result = true break } } if !result { return } } for k, fn := range c.BeforeFilters { if f.FilterMatched(strings.Split(k, "|")...) { fn() } } if len(f.Tags) > 0 { c.line(clrCyan, strings.Join([]string(f.Tags), " ")) } c.line("0;1", "Feature: %s", f.Title) if f.Background.Steps != nil { c.runScenario("Background", f, &f.Background, false) } for _, s := range f.Scenarios { c.runScenario("Scenario", f, &s, false) } for k, fn := range c.AfterFilters { if f.FilterMatched(strings.Split(k, "|")...) { fn() } } } func (c *Runner) runScenario(title string, f *gherkin.Feature, s *gherkin.Scenario, isExample bool) { if !s.FilterMatched(f, c.Filters...) { return } for k, fn := range c.BeforeFilters { if s.FilterMatched(f, strings.Split(k, "|")...) { fn() } } if s.Examples != "" { // run scenario outline data exrows := strings.Split(string(s.Examples), "\n") c.line(clrCyan, " "+strings.Join([]string(s.Tags), " ")) c.fileLine("0;1", " %s Outline: %s", s.Filename, s.Line, s.LongestLine()+1, title, s.Title) for _, step := range s.Steps { c.fileLine("0;0", " %s %s", step.Filename, step.Line, s.LongestLine()+1, step.Type, step.Text) } c.line(clrWhite, "") c.line("0;1", " Examples:") c.line(clrCyan, " %s", exrows[0]) tab := s.Examples.ToTable() tabmap := tab.ToMap() for i, rows := 1, len(tab); i < rows; i++ { other := gherkin.Scenario{ Filename: s.Filename, Line: s.Line, Title: s.Title, Examples: gherkin.StringData(""), Steps: []gherkin.Step{}, } for _, step := range s.Steps { step.Text = reOutlineVal.ReplaceAllStringFunc(step.Text, func(t string) string { return tabmap[t[1:len(t)-1]][i-1] }) other.Steps = append(other.Steps, step) } fc := c.FailCount clr := clrGreen c.runScenario(title, f, &other, true) if fc != c.FailCount { clr = clrRed } c.line(clr, " %s", exrows[i]) } c.line(clrWhite, "") for k, fn := range c.AfterFilters { if s.FilterMatched(f, strings.Split(k, "|")...) { fn() } } return } t := &TestingT{} skipping := false clr := clrGreen if !isExample { if len(s.Tags) > 0 { c.line(clrCyan, " "+strings.Join([]string(s.Tags), " ")) } c.fileLine("0;1", " %s: %s", s.Filename, s.Line, s.LongestLine(), title, s.Title) } for _, step := range s.Steps { errCount := len(t.errors) found := false if !skipping && !t.Failed() { done := make(chan bool) go func() { defer func() { c.Results = append(c.Results, RunnerResult{t, f, s}) if t.Skipped() { c.SkipCount++ skipping = true clr = clrYellow } else if t.Failed() { c.FailCount++ clr = clrRed } done <- true }() f, err := c.Execute(t, step.Text, string(step.Argument)) if err != nil { t.Error(err) } found = f if !f { t.Skip("no match function for step") } }() <-done } if skipping && !found { cstep := step c.Unmatched = append(c.Unmatched, &cstep) } if !isExample { c.fileLine(clr, " %s %s", step.Filename, step.Line, s.LongestLine(), step.Type, step.Text) if len(step.Argument) > 0 { if !step.Argument.IsTabular() { c.line(clrWhite, ` """`) } for _, l := range strings.Split(string(step.Argument), "\n") { c.line(clrWhite, " %s", l) } if !step.Argument.IsTabular() { c.line(clrWhite, ` """`) } } } if len(t.errors) > errCount { c.line(clrRed, "\n"+t.errors[len(t.errors)-1].message) } } if !isExample { c.line(clrWhite, "") } for k, fn := range c.AfterFilters { if s.FilterMatched(f, strings.Split(k, "|")...) { fn() } } } var writer = ansicolor.NewAnsiColorWriter(os.Stdout) func (c *Runner) line(clr, text string, args ...interface{}) { fmt.Fprintf(writer, "\033[%sm%s\033[0;0m\n", clr, fmt.Sprintf(text, args...)) } func (c *Runner) fileLine(clr, text, filename string, line int, max int, args ...interface{}) { space, str := "", fmt.Sprintf(text, args...) if l := max + 5 - len(str); l > 0 { space = strings.Repeat(" ", l) } comment := fmt.Sprintf("%s \033[39;0m# %s:%d", space, filename, line) c.line(clr, "%s%s", str, comment) } type Tester interface { Errorf(format string, args ...interface{}) Skip(args ...interface{}) } type TestingT struct { skipped bool errors []TestError } type TestError struct { message string stack []byte } func (t *TestingT) Errorf(format string, args ...interface{}) { var buf bytes.Buffer str := fmt.Sprintf(format, args...) sbuf := make([]byte, 8192) for { size := runtime.Stack(sbuf, false) if size < len(sbuf) { break } buf.Write(sbuf[0:size]) } t.errors = append(t.errors, TestError{message: str, stack: buf.Bytes()}) } func (t *TestingT) Skip(args ...interface{}) { t.skipped = true } func (t *TestingT) Skipped() bool { return t.skipped } func (t *TestingT) Failed() bool { return len(t.errors) > 0 } func (t *TestingT) Error(err error) { t.errors = append(t.errors, TestError{message: err.Error()}) } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/stepdef.go000066400000000000000000000112671274204156200227010ustar00rootroot00000000000000package gucumber import ( "fmt" "reflect" "regexp" "strconv" "strings" "github.com/gucumber/gucumber/gherkin" ) var ( GlobalContext = Context{ Steps: []StepDefinition{}, World: map[string]interface{}{}, BeforeFilters: map[string]func(){}, AfterFilters: map[string]func(){}, Filters: []string{}, } T Tester World = GlobalContext.World errNoMatchingStepFns = fmt.Errorf("no functions matched step.") ) func Given(match string, fn interface{}) { GlobalContext.Given(match, fn) } func Then(match string, fn interface{}) { GlobalContext.Then(match, fn) } func When(match string, fn interface{}) { GlobalContext.When(match, fn) } func And(match string, fn interface{}) { GlobalContext.And(match, fn) } func Before(filter string, fn func()) { GlobalContext.Before(filter, fn) } func After(filter string, fn func()) { GlobalContext.After(filter, fn) } func BeforeMulti(filters []string, fn func()) { GlobalContext.BeforeMulti(filters, fn) } func AfterMulti(filters []string, fn func()) { GlobalContext.AfterMulti(filters, fn) } func BeforeAll(fn func()) { GlobalContext.BeforeAll(fn) } func AfterAll(fn func()) { GlobalContext.AfterAll(fn) } func Execute(t Tester, line string, arg string) (bool, error) { return GlobalContext.Execute(t, line, arg) } type Context struct { Filters []string World map[string]interface{} BeforeFilters map[string]func() AfterFilters map[string]func() BeforeAllFilter func() AfterAllFilter func() Steps []StepDefinition T Tester } func (c *Context) addStep(match string, fn interface{}) { c.Steps = append(c.Steps, StepDefinition{ Matcher: regexp.MustCompile(match), Function: reflect.ValueOf(fn), }) } func (c *Context) Given(match string, fn interface{}) { c.addStep(match, fn) } func (c *Context) Then(match string, fn interface{}) { c.addStep(match, fn) } func (c *Context) When(match string, fn interface{}) { c.addStep(match, fn) } func (c *Context) And(match string, fn interface{}) { c.addStep(match, fn) } func (c *Context) Before(filter string, fn func()) { c.BeforeFilters[filter] = fn } func (c *Context) After(filter string, fn func()) { c.AfterFilters[filter] = fn } func (c *Context) BeforeMulti(filters []string, fn func()) { c.BeforeFilters[strings.Join(filters, "|")] = fn } func (c *Context) AfterMulti(filters []string, fn func()) { c.AfterFilters[strings.Join(filters, "|")] = fn } func (c *Context) BeforeAll(fn func()) { c.BeforeAllFilter = fn } func (c *Context) AfterAll(fn func()) { c.AfterAllFilter = fn } func (c *Context) Execute(t Tester, line string, arg string) (bool, error) { T = t c.T = t found := false for _, step := range c.Steps { f, err := step.CallIfMatch(c, t, line, arg) if err != nil { return f, err } if f { found = true } } return found, nil } type StepDefinition struct { Matcher *regexp.Regexp Function reflect.Value } func (s *StepDefinition) CallIfMatch(c *Context, test Tester, line string, arg string) (bool, error) { if match := s.Matcher.FindStringSubmatch(line); match != nil { match = match[1:] // discard full line match // adjust arity if there is step arg data numArgs := len(match) if arg != "" { numArgs++ } t := s.Function.Type() if t.NumIn() > 0 && t.In(0).Kind() == reflect.Ptr { e := t.In(0).Elem() if e.String() == "testing.T" { numArgs++ // first param is *testing.T } } if numArgs != t.NumIn() { // function has different arity return true, fmt.Errorf("matcher function has different arity %d != %d", numArgs, t.NumIn()) } values := make([]reflect.Value, numArgs) for m, i := 0, 0; i < t.NumIn(); i++ { param := t.In(i) var v interface{} switch param.Kind() { case reflect.Slice: param = param.Elem() if param.String() == "gherkin.TabularData" { v = gherkin.StringData(arg).ToTable() } else if param.Kind() == reflect.Slice && param.Elem().Kind() == reflect.String { // just a raw [][]string slice v = gherkin.StringData(arg).ToTable() } case reflect.Ptr: if param.Elem().String() == "testing.T" { v = test } case reflect.Int: i, _ := strconv.ParseInt(match[m], 10, 32) v = int(i) m++ case reflect.Int64: v, _ = strconv.ParseInt(match[m], 10, 64) m++ case reflect.String: // this could be from `arg`, check match index if m >= len(match) { v = arg } else { v = match[m] m++ } case reflect.Float64: v, _ = strconv.ParseFloat(match[m], 64) m++ } if v == nil { panic("type " + t.String() + "is not supported.") } values[i] = reflect.ValueOf(v) } s.Function.Call(values) return true, nil } return false, nil } gucumber-71608e2f6e76fd4da5b09a376aeec7a5c0b5edbc/stepdef_test.go000066400000000000000000000043531274204156200237360ustar00rootroot00000000000000package gucumber_test import ( "testing" . "github.com/gucumber/gucumber" "github.com/gucumber/gucumber/gherkin" "github.com/stretchr/testify/assert" ) func TestRegisterSteps(t *testing.T) { count := 0 str := "" fl := 0.0 Given(`^I have a test with (\d+)$`, func(i int) { count += i }) When(`^I have a condition of (\d+) with decimal (-?\d+\.\d+)$`, func(i int64, f float64) { count += int(i); fl = f }) And(`^I have another condition with "(.+?)"$`, func(s string) { str = s }) Then(`^something will happen with text$`, func(data string) { str += " " + data }) And(`^something will happen with a table:$`, func(table gherkin.TabularData) { str += " " + table[0][0] + table[0][1] + table[1][0] + table[1][1] }) And(`^something will happen with a table:$`, func(table [][]string) { str += " " + table[0][0] + table[0][1] + table[1][0] + table[1][1] }) And(`^I can pass in test (.+?)$`, func(tt *testing.T, data string) { assert.Equal(t, t, tt) str += " " + data }) found, err := GlobalContext.Execute(t, "I have a test with 3", "") GlobalContext.Execute(t, "I have a condition of 5 with decimal -3.14159", "") GlobalContext.Execute(t, "I have another condition with \"arbitrary text\"", "") GlobalContext.Execute(t, "something will happen with text", "and hello world") GlobalContext.Execute(t, "something will happen with a table:", "| a | b |\n| c | d |") GlobalContext.Execute(t, "I can pass in test context", "") assert.NoError(t, err) assert.Equal(t, true, found) assert.Equal(t, 8, count) assert.Equal(t, "arbitrary text and hello world abcd abcd context", str) assert.Equal(t, -3.14159, fl) } func TestGlobalExecute(t *testing.T) { count := 0 Given(`^I have a test with (\d+)$`, func(i int) { count += i }) found, err := Execute(t, "I have a test with 3", "") assert.NoError(t, err) assert.Equal(t, true, found) } func TestDifferentArgCount(t *testing.T) { Given(`^an arg mismatch$`, func(i int) {}) f, err := GlobalContext.Execute(t, "an arg mismatch", "") assert.EqualError(t, err, "matcher function has different arity 0 != 1") assert.Equal(t, true, f) } func TestNoMatches(t *testing.T) { f, err := GlobalContext.Execute(t, "something that won't be matched", "") assert.NoError(t, err) assert.Equal(t, false, f) }