You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
272 lines
7.0 KiB
272 lines
7.0 KiB
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)
|
|
}
|
|
}
|
|
|