diff --git a/convey/context.go b/convey/context.go index 65e1e57c..94e8de08 100644 --- a/convey/context.go +++ b/convey/context.go @@ -1,7 +1,11 @@ package convey import ( + "errors" + "fmt" "runtime" + "strconv" + "strings" "sync" "github.com/smartystreets/goconvey/execution" @@ -13,31 +17,36 @@ import ( // that each test function has its own runner and reporter, and routes all live registrations // to the appropriate runner/reporter. type suiteContext struct { - runners map[string]execution.Runner - reporters map[string]reporting.Reporter + locations map[string]string // key: file:line; value: testName + runners map[string]execution.Runner // key: testName; + reporters map[string]reporting.Reporter // key: testName; lock sync.Mutex } func (self *suiteContext) Run(entry *execution.Registration) { - key := resolveTestPackageAndFunctionName() + testName, location, _ := resolveAnchorConvey() + if self.currentRunner() != nil { panic(execution.ExtraGoTest) } + reporter := buildReporter() runner := execution.NewRunner() runner.UpgradeReporter(reporter) self.lock.Lock() - self.runners[key] = runner - self.reporters[key] = reporter + self.locations[location] = testName + self.runners[testName] = runner + self.reporters[testName] = reporter self.lock.Unlock() runner.Begin(entry) runner.Run() self.lock.Lock() - delete(self.runners, key) - delete(self.reporters, key) + delete(self.locations, location) + delete(self.runners, testName) + delete(self.reporters, testName) self.lock.Unlock() } @@ -53,46 +62,90 @@ func (self *suiteContext) CurrentRunner() execution.Runner { func (self *suiteContext) currentRunner() execution.Runner { self.lock.Lock() defer self.lock.Unlock() - return self.runners[resolveTestPackageAndFunctionName()] + testName, _, _ := resolveAnchorConvey() + return self.runners[testName] } func (self *suiteContext) CurrentReporter() reporting.Reporter { self.lock.Lock() defer self.lock.Unlock() - return self.reporters[resolveTestPackageAndFunctionName()] + testName, _, err := resolveAnchorConvey() + + if err != nil { + file, line := resolveTestFileAndLine() + closest := -1 + for location, registeredTestName := range self.locations { + parts := strings.Split(location, ":") + locationFile := parts[0] + if locationFile != file { + continue + } + + locationLine, err := strconv.Atoi(parts[1]) + if err != nil || locationLine < line { + continue + } + + if closest == -1 || locationLine < closest { + closest = locationLine + testName = registeredTestName + } + } + } + + return self.reporters[testName] } func newSuiteContext() *suiteContext { self := new(suiteContext) + self.locations = make(map[string]string) self.runners = make(map[string]execution.Runner) self.reporters = make(map[string]reporting.Reporter) return self } -// resolveTestPackageAndFunctionName traverses the call stack in reverse, looking for +// resolveAnchorConvey traverses the call stack in reverse, looking for // the go testing harnass call ("testing.tRunner") and then grabs the very next entry, -// which represents the package under test and the test function name. Voila! -func resolveTestPackageAndFunctionName() string { - var ( - callerId uintptr - found bool - ) +// which represents the test function name, including the package name as a prefix. +// It also returns the file:line combo of the top-level Convey. Voila! +func resolveAnchorConvey() (testName, location string, err error) { + callers := runtime.Callers(0, callStack) + + for y := callers; y > 0; y-- { + callerId, file, conveyLine, found := runtime.Caller(y) + if !found { + continue + } + + if name := runtime.FuncForPC(callerId).Name(); name != goTestHarness { + continue + } + + callerId, file, conveyLine, _ = runtime.Caller(y - 1) + testName = runtime.FuncForPC(callerId).Name() + location = fmt.Sprintf("%s:%d", file, conveyLine) + return + } + return "", "", errors.New("Can't resolve test method name! Are you calling Convey() from a `*_test.go` file and a `Test*` method (because you should be)?") +} + +// resolveTestFileAndLine is used as a last-ditch effort to correlate an +// assertion with the right executor and runner. +func resolveTestFileAndLine() (file string, line int) { callers := runtime.Callers(0, callStack) + var found bool for y := callers; y > 0; y-- { - callerId, _, _, found = runtime.Caller(y) + _, file, line, found = runtime.Caller(y) if !found { continue } - packageAndTestFunctionName := runtime.FuncForPC(callerId).Name() - if packageAndTestFunctionName == goTestHarness { - callerId, _, _, _ = runtime.Caller(y - 1) - name := runtime.FuncForPC(callerId).Name() - return name + if strings.HasSuffix(file, "_test.go") { + return } } - panic("Can't resolve test method name! Are you calling Convey() from a `*_test.go` file and a `Test*` method (because you should be)?") + return "", 0 } const maxStackDepth = 100 // This had better be enough... diff --git a/convey/reporting_hooks_test.go b/convey/reporting_hooks_test.go index 911a155b..26bb9217 100644 --- a/convey/reporting_hooks_test.go +++ b/convey/reporting_hooks_test.go @@ -2,6 +2,8 @@ package convey import ( "fmt" + "net/http" + "net/http/httptest" "path" "runtime" "strconv" @@ -175,6 +177,19 @@ func TestIterativeConveysReported(t *testing.T) { expectEqual(t, "Begin|A|0|Success|Exit|Exit|A|1|Success|Exit|Exit|A|2|Success|Exit|Exit|End", myReporter.wholeStory()) } +func TestEmbeddedAssertionReported(t *testing.T) { + myReporter, test := setupFakeReporter() + + Convey("A", test, func() { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + So(r.FormValue("msg"), ShouldEqual, "ping") + })) + http.DefaultClient.Get(ts.URL + "?msg=ping") + }) + + expectEqual(t, "Begin|A|Success|Exit|End", myReporter.wholeStory()) +} + func expectEqual(t *testing.T, expected interface{}, actual interface{}) { if expected != actual { _, file, line, _ := runtime.Caller(1)