mirror of https://github.com/gogits/gogs.git
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) |
|
} |
|
}
|
|
|