diff --git a/fmt.go b/fmt.go index 2b5d9af..f1082d9 100644 --- a/fmt.go +++ b/fmt.go @@ -84,8 +84,9 @@ func Format(name, description string, f Formatter) { // formatters needs to be registered with a // godog.Format function call type Formatter interface { - Feature(*gherkin.Feature, string) + Feature(*gherkin.Feature, string, []byte) Node(interface{}) + Defined(*gherkin.Step, *StepDef) Failed(*gherkin.Step, *StepDef, error) Passed(*gherkin.Step, *StepDef) Skipped(*gherkin.Step) @@ -117,6 +118,23 @@ func (st stepType) clr() color { } } +func (st stepType) String() string { + switch st { + case passed: + return "passed" + case failed: + return "failed" + case skipped: + return "skipped" + case undefined: + return "undefined" + case pending: + return "pending" + default: + return "unknown" + } +} + type stepResult struct { typ stepType feature *feature @@ -152,7 +170,11 @@ func (f *basefmt) Node(n interface{}) { } } -func (f *basefmt) Feature(ft *gherkin.Feature, p string) { +func (f *basefmt) Defined(*gherkin.Step, *StepDef) { + +} + +func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) { f.features = append(f.features, &feature{Path: p, Feature: ft}) } diff --git a/fmt_events.go b/fmt_events.go new file mode 100644 index 0000000..d495779 --- /dev/null +++ b/fmt_events.go @@ -0,0 +1,256 @@ +package godog + +import ( + "crypto/sha1" + "encoding/hex" + "encoding/json" + "fmt" + "sync" + "time" + + "gopkg.in/cucumber/gherkin-go.v3" +) + +const nanoSec = 1000000 + +func init() { + Format("events", "Produces a JSON event stream.", &events{ + basefmt: basefmt{ + started: time.Now(), + indent: 2, + }, + runID: sha1RunID(), + suite: "main", + }) +} + +func sha1RunID() string { + data, err := json.Marshal(&struct { + Time int64 + Runner string + }{time.Now().UnixNano(), "godog"}) + + if err != nil { + panic("failed to marshal run id") + } + + hasher := sha1.New() + hasher.Write(data) + return hex.EncodeToString(hasher.Sum(nil)) +} + +type events struct { + sync.Once + basefmt + + runID string + suite string + + // currently running feature path, to be part of id. + // this is sadly not passed by gherkin nodes. + // it restricts this formatter to run only in synchronous single + // threaded execution. Unless running a copy of formatter for each feature + path string +} + +func (f *events) event(ev interface{}) { + data, err := json.Marshal(ev) + if err != nil { + panic(fmt.Sprintf("failed to marshal stream event: %+v - %v", ev, err)) + } + fmt.Println(string(data)) +} + +func (f *events) started() { + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Version string `json:"version"` + Timestamp int64 `json:"timestamp"` + }{ + "TestRunStarted", + f.runID, + "0.1.0", + time.Now().UnixNano() / nanoSec, + }) +} + +func (f *events) Node(n interface{}) { + f.basefmt.Node(n) + f.Do(f.started) + + switch t := n.(type) { + case *gherkin.Scenario: + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Suite string `json:"suite"` + Location string `json:"location"` + Timestamp int64 `json:"timestamp"` + }{ + "TestCaseStarted", + f.runID, + "main", + fmt.Sprintf("%s:%d", f.path, t.Location.Line), + time.Now().UnixNano() / nanoSec, + }) + case *gherkin.TableRow: + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Suite string `json:"suite"` + Location string `json:"location"` + Timestamp int64 `json:"timestamp"` + }{ + "TestCaseStarted", + f.runID, + "main", + fmt.Sprintf("%s:%d", f.path, t.Location.Line), + time.Now().UnixNano() / nanoSec, + }) + } +} + +func (f *events) Feature(ft *gherkin.Feature, p string, c []byte) { + f.basefmt.Feature(ft, p, c) + f.path = p + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Location string `json:"location"` + Source string `json:"source"` + }{ + "TestSource", + f.runID, + fmt.Sprintf("%s:%d", p, ft.Location.Line), + string(c), + }) +} + +func (f *events) Summary() { + // determine status + status := passed + if len(f.failed) > 0 { + status = failed + } else if len(f.passed) == 0 { + if len(f.undefined) > len(f.pending) { + status = undefined + } else { + status = pending + } + } + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Status string `json:"status"` + Timestamp int64 `json:"timestamp"` + }{ + "TestRunFinished", + f.runID, + status.String(), + time.Now().UnixNano() / nanoSec, + }) +} + +func (f *events) step(res *stepResult) { + var errMsg string + if res.err != nil { + errMsg = res.err.Error() + } + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Suite string `json:"suite"` + Location string `json:"location"` + Timestamp int64 `json:"timestamp"` + Status string `json:"status"` + Summary string `json:"summary,omitempty"` + }{ + "TestStepFinished", + f.runID, + "main", + fmt.Sprintf("%s:%d", f.path, res.step.Location.Line), + time.Now().UnixNano() / nanoSec, + res.typ.String(), + errMsg, + }) + + switch t := f.owner.(type) { + case *gherkin.ScenarioOutline: + case *gherkin.Scenario: + for i, s := range t.Steps { + if s.Location.Line == res.step.Location.Line && i == len(t.Steps)-1 { + // what if scenario is empty + } + } + } +} + +func (f *events) Defined(step *gherkin.Step, def *StepDef) { + if def != nil { + m := def.Expr.FindStringSubmatchIndex(step.Text)[2:] + args := make([][2]int, 0) + for i := 0; i < len(m)/2; i++ { + pair := m[i : i*2+2] + var idxs [2]int + idxs[0] = pair[0] + idxs[1] = pair[1] + args = append(args, idxs) + } + + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Suite string `json:"suite"` + Location string `json:"location"` + DefID string `json:"definition_id"` + Args [][2]int `json:"arguments"` + }{ + "StepDefinitionFound", + f.runID, + "main", + fmt.Sprintf("%s:%d", f.path, step.Location.Line), + def.definitionID(), + args, + }) + } + + f.event(&struct { + Event string `json:"event"` + RunID string `json:"run_id"` + Suite string `json:"suite"` + Location string `json:"location"` + Timestamp int64 `json:"timestamp"` + }{ + "TestStepStarted", + f.runID, + "main", + fmt.Sprintf("%s:%d", f.path, step.Location.Line), + time.Now().UnixNano() / nanoSec, + }) +} + +func (f *events) Passed(step *gherkin.Step, match *StepDef) { + f.basefmt.Passed(step, match) + f.step(f.passed[len(f.passed)-1]) +} + +func (f *events) Skipped(step *gherkin.Step) { + f.basefmt.Skipped(step) + f.step(f.skipped[len(f.skipped)-1]) +} + +func (f *events) Undefined(step *gherkin.Step) { + f.basefmt.Undefined(step) + f.step(f.undefined[len(f.undefined)-1]) +} + +func (f *events) Failed(step *gherkin.Step, match *StepDef, err error) { + f.basefmt.Failed(step, match, err) + f.step(f.failed[len(f.failed)-1]) +} + +func (f *events) Pending(step *gherkin.Step, match *StepDef) { + f.basefmt.Pending(step, match) + f.step(f.pending[len(f.pending)-1]) +} diff --git a/fmt_junit.go b/fmt_junit.go index d0130dd..cfaa430 100644 --- a/fmt_junit.go +++ b/fmt_junit.go @@ -32,7 +32,7 @@ type junitFormatter struct { outlineExample int } -func (j *junitFormatter) Feature(feature *gherkin.Feature, path string) { +func (j *junitFormatter) Feature(feature *gherkin.Feature, path string, c []byte) { testSuite := &junitTestSuite{ TestCases: make([]*junitTestCase, 0), Name: feature.Name, @@ -45,6 +45,10 @@ func (j *junitFormatter) Feature(feature *gherkin.Feature, path string) { j.suite.TestSuites = append(j.suite.TestSuites, testSuite) } +func (f *junitFormatter) Defined(*gherkin.Step, *StepDef) { + +} + func (j *junitFormatter) Node(node interface{}) { suite := j.current() tcase := &junitTestCase{} diff --git a/fmt_pretty.go b/fmt_pretty.go index 8889ce8..c8e45d2 100644 --- a/fmt_pretty.go +++ b/fmt_pretty.go @@ -45,7 +45,7 @@ type pretty struct { outlineNumExamples int } -func (f *pretty) Feature(ft *gherkin.Feature, p string) { +func (f *pretty) Feature(ft *gherkin.Feature, p string, c []byte) { if len(f.features) != 0 { // not a first feature, add a newline fmt.Println("") @@ -168,7 +168,7 @@ func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) { } else { text = cl(ostep.Text, cyan) } - text += s(f.commentPos-f.length(ostep)+1) + cl(fmt.Sprintf("# %s", res.def.funcName()), black) + text += s(f.commentPos-f.length(ostep)+1) + cl(fmt.Sprintf("# %s", res.def.definitionID()), black) } else { text = cl(ostep.Text, cyan) } @@ -207,7 +207,7 @@ func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) { text := s(f.indent*2) + cl(strings.TrimSpace(step.Keyword), c) + " " switch { case def != nil: - if m := (def.Expr.FindStringSubmatchIndex(step.Text))[2:]; len(m) > 0 { + if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 { var pos, i int for pos, i = 0, 0; i < len(m); i++ { if math.Mod(float64(i), 2) == 0 { @@ -221,7 +221,7 @@ func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c color) { } else { text += cl(step.Text, c) } - text += s(f.commentPos-f.length(step)+1) + cl(fmt.Sprintf("# %s", def.funcName()), black) + text += s(f.commentPos-f.length(step)+1) + cl(fmt.Sprintf("# %s", def.definitionID()), black) default: text += cl(step.Text, c) } diff --git a/fmt_progress.go b/fmt_progress.go index 36c8b12..0a23e0e 100644 --- a/fmt_progress.go +++ b/fmt_progress.go @@ -32,10 +32,10 @@ func (f *progress) Node(n interface{}) { f.basefmt.Node(n) } -func (f *progress) Feature(ft *gherkin.Feature, p string) { +func (f *progress) Feature(ft *gherkin.Feature, p string, c []byte) { f.Lock() defer f.Unlock() - f.basefmt.Feature(ft, p) + f.basefmt.Feature(ft, p, c) } func (f *progress) Summary() { diff --git a/stepdef.go b/stepdef.go index 6ce06ce..4ba849d 100644 --- a/stepdef.go +++ b/stepdef.go @@ -29,7 +29,7 @@ type StepDef struct { Handler interface{} } -func (sd *StepDef) funcName() string { +func (sd *StepDef) definitionID() string { ptr := sd.hv.Pointer() f := runtime.FuncForPC(ptr) file, line := f.FileLine(ptr) diff --git a/suite.go b/suite.go index 3ceaba5..c9d600d 100644 --- a/suite.go +++ b/suite.go @@ -1,7 +1,9 @@ package godog import ( + "bytes" "fmt" + "io" "os" "path/filepath" "reflect" @@ -19,7 +21,8 @@ var typeOfBytes = reflect.TypeOf([]byte(nil)) type feature struct { *gherkin.Feature - Path string `json:"path"` + Content []byte `json:"-"` + Path string `json:"path"` } // ErrUndefined is returned in case if step definition was not found @@ -193,11 +196,13 @@ func (s *Suite) matchStep(step *gherkin.Step) *StepDef { return h } } + // @TODO can handle ambiguous return nil } func (s *Suite) runStep(step *gherkin.Step, prevStepErr error) (err error) { match := s.matchStep(step) + s.fmt.Defined(step, match) if match == nil { s.fmt.Undefined(step) return ErrUndefined @@ -325,7 +330,7 @@ func (s *Suite) runOutline(outline *gherkin.ScenarioOutline, b *gherkin.Backgrou } func (s *Suite) runFeature(f *feature) { - s.fmt.Feature(f.Feature, f.Path) + s.fmt.Feature(f.Feature, f.Path, f.Content) for _, scenario := range f.ScenarioDefinitions { var err error if f.Background != nil { @@ -408,12 +413,13 @@ func parseFeatures(filter string, paths []string) (features []*feature, err erro if err != nil { return err } - ft, err := gherkin.ParseFeature(reader) + var buf bytes.Buffer + ft, err := gherkin.ParseFeature(io.TeeReader(reader, &buf)) reader.Close() if err != nil { return err } - features = append(features, &feature{Path: p, Feature: ft}) + features = append(features, &feature{Path: p, Feature: ft, Content: buf.Bytes()}) // filter scenario by line number if line != -1 { var scenarios []interface{}