123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- package convey
- import (
- "fmt"
- "github.com/jtolds/gls"
- "github.com/smartystreets/goconvey/convey/reporting"
- )
- type conveyErr struct {
- fmt string
- params []interface{}
- }
- func (e *conveyErr) Error() string {
- return fmt.Sprintf(e.fmt, e.params...)
- }
- func conveyPanic(fmt string, params ...interface{}) {
- panic(&conveyErr{fmt, params})
- }
- const (
- missingGoTest = `Top-level calls to Convey(...) need a reference to the *testing.T.
- Hint: Convey("description here", t, func() { /* notice that the second argument was the *testing.T (t)! */ }) `
- extraGoTest = `Only the top-level call to Convey(...) needs a reference to the *testing.T.`
- noStackContext = "Convey operation made without context on goroutine stack.\n" +
- "Hint: Perhaps you meant to use `Convey(..., func(c C){...})` ?"
- differentConveySituations = "Different set of Convey statements on subsequent pass!\nDid not expect %#v."
- multipleIdenticalConvey = "Multiple convey suites with identical names: %#v"
- )
- const (
- failureHalt = "___FAILURE_HALT___"
- nodeKey = "node"
- )
- ///////////////////////////////// Stack Context /////////////////////////////////
- func getCurrentContext() *context {
- ctx, ok := ctxMgr.GetValue(nodeKey)
- if ok {
- return ctx.(*context)
- }
- return nil
- }
- func mustGetCurrentContext() *context {
- ctx := getCurrentContext()
- if ctx == nil {
- conveyPanic(noStackContext)
- }
- return ctx
- }
- //////////////////////////////////// Context ////////////////////////////////////
- // context magically handles all coordination of Convey's and So assertions.
- //
- // It is tracked on the stack as goroutine-local-storage with the gls package,
- // or explicitly if the user decides to call convey like:
- //
- // Convey(..., func(c C) {
- // c.So(...)
- // })
- //
- // This implements the `C` interface.
- type context struct {
- reporter reporting.Reporter
- children map[string]*context
- resets []func()
- executedOnce bool
- expectChildRun *bool
- complete bool
- focus bool
- failureMode FailureMode
- }
- // rootConvey is the main entry point to a test suite. This is called when
- // there's no context in the stack already, and items must contain a `t` object,
- // or this panics.
- func rootConvey(items ...interface{}) {
- entry := discover(items)
- if entry.Test == nil {
- conveyPanic(missingGoTest)
- }
- expectChildRun := true
- ctx := &context{
- reporter: buildReporter(),
- children: make(map[string]*context),
- expectChildRun: &expectChildRun,
- focus: entry.Focus,
- failureMode: defaultFailureMode.combine(entry.FailMode),
- }
- ctxMgr.SetValues(gls.Values{nodeKey: ctx}, func() {
- ctx.reporter.BeginStory(reporting.NewStoryReport(entry.Test))
- defer ctx.reporter.EndStory()
- for ctx.shouldVisit() {
- ctx.conveyInner(entry.Situation, entry.Func)
- expectChildRun = true
- }
- })
- }
- //////////////////////////////////// Methods ////////////////////////////////////
- func (ctx *context) SkipConvey(items ...interface{}) {
- ctx.Convey(items, skipConvey)
- }
- func (ctx *context) FocusConvey(items ...interface{}) {
- ctx.Convey(items, focusConvey)
- }
- func (ctx *context) Convey(items ...interface{}) {
- entry := discover(items)
- // we're a branch, or leaf (on the wind)
- if entry.Test != nil {
- conveyPanic(extraGoTest)
- }
- if ctx.focus && !entry.Focus {
- return
- }
- var inner_ctx *context
- if ctx.executedOnce {
- var ok bool
- inner_ctx, ok = ctx.children[entry.Situation]
- if !ok {
- conveyPanic(differentConveySituations, entry.Situation)
- }
- } else {
- if _, ok := ctx.children[entry.Situation]; ok {
- conveyPanic(multipleIdenticalConvey, entry.Situation)
- }
- inner_ctx = &context{
- reporter: ctx.reporter,
- children: make(map[string]*context),
- expectChildRun: ctx.expectChildRun,
- focus: entry.Focus,
- failureMode: ctx.failureMode.combine(entry.FailMode),
- }
- ctx.children[entry.Situation] = inner_ctx
- }
- if inner_ctx.shouldVisit() {
- ctxMgr.SetValues(gls.Values{nodeKey: inner_ctx}, func() {
- inner_ctx.conveyInner(entry.Situation, entry.Func)
- })
- }
- }
- func (ctx *context) SkipSo(stuff ...interface{}) {
- ctx.assertionReport(reporting.NewSkipReport())
- }
- func (ctx *context) So(actual interface{}, assert assertion, expected ...interface{}) {
- if result := assert(actual, expected...); result == assertionSuccess {
- ctx.assertionReport(reporting.NewSuccessReport())
- } else {
- ctx.assertionReport(reporting.NewFailureReport(result))
- }
- }
- func (ctx *context) Reset(action func()) {
- /* TODO: Failure mode configuration */
- ctx.resets = append(ctx.resets, action)
- }
- func (ctx *context) Print(items ...interface{}) (int, error) {
- fmt.Fprint(ctx.reporter, items...)
- return fmt.Print(items...)
- }
- func (ctx *context) Println(items ...interface{}) (int, error) {
- fmt.Fprintln(ctx.reporter, items...)
- return fmt.Println(items...)
- }
- func (ctx *context) Printf(format string, items ...interface{}) (int, error) {
- fmt.Fprintf(ctx.reporter, format, items...)
- return fmt.Printf(format, items...)
- }
- //////////////////////////////////// Private ////////////////////////////////////
- // shouldVisit returns true iff we should traverse down into a Convey. Note
- // that just because we don't traverse a Convey this time, doesn't mean that
- // we may not traverse it on a subsequent pass.
- func (c *context) shouldVisit() bool {
- return !c.complete && *c.expectChildRun
- }
- // conveyInner is the function which actually executes the user's anonymous test
- // function body. At this point, Convey or RootConvey has decided that this
- // function should actually run.
- func (ctx *context) conveyInner(situation string, f func(C)) {
- // Record/Reset state for next time.
- defer func() {
- ctx.executedOnce = true
- // This is only needed at the leaves, but there's no harm in also setting it
- // when returning from branch Convey's
- *ctx.expectChildRun = false
- }()
- // Set up+tear down our scope for the reporter
- ctx.reporter.Enter(reporting.NewScopeReport(situation))
- defer ctx.reporter.Exit()
- // Recover from any panics in f, and assign the `complete` status for this
- // node of the tree.
- defer func() {
- ctx.complete = true
- if problem := recover(); problem != nil {
- if problem, ok := problem.(*conveyErr); ok {
- panic(problem)
- }
- if problem != failureHalt {
- ctx.reporter.Report(reporting.NewErrorReport(problem))
- }
- } else {
- for _, child := range ctx.children {
- if !child.complete {
- ctx.complete = false
- return
- }
- }
- }
- }()
- // Resets are registered as the `f` function executes, so nil them here.
- // All resets are run in registration order (FIFO).
- ctx.resets = []func(){}
- defer func() {
- for _, r := range ctx.resets {
- // panics handled by the previous defer
- r()
- }
- }()
- if f == nil {
- // if f is nil, this was either a Convey(..., nil), or a SkipConvey
- ctx.reporter.Report(reporting.NewSkipReport())
- } else {
- f(ctx)
- }
- }
- // assertionReport is a helper for So and SkipSo which makes the report and
- // then possibly panics, depending on the current context's failureMode.
- func (ctx *context) assertionReport(r *reporting.AssertionResult) {
- ctx.reporter.Report(r)
- if r.Failure != "" && ctx.failureMode == FailureHalts {
- panic(failureHalt)
- }
- }
|