From 238c6ef2bfb00331b80a375344e164a8585178a8 Mon Sep 17 00:00:00 2001 From: Mike Tonks Date: Mon, 4 Nov 2019 10:58:14 +0000 Subject: [PATCH] Add StackMode to make stack traces configurable --- convey/context.go | 9 +- convey/discovery.go | 15 +++- convey/doc.go | 38 +++++++++ convey/reporting/dot_test.go | 2 +- convey/reporting/gotest_test.go | 2 +- convey/reporting/init.go | 3 +- convey/reporting/problems.go | 5 +- convey/reporting/problems_test.go | 2 +- convey/reporting/reports.go | 6 +- convey/stack_trace_test.go | 137 ++++++++++++++++++++++++++++++ examples/stack_test.go | 15 ++++ 11 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 convey/stack_trace_test.go create mode 100644 examples/stack_test.go diff --git a/convey/context.go b/convey/context.go index 2c75c2d7..a0f8b103 100644 --- a/convey/context.go +++ b/convey/context.go @@ -79,6 +79,7 @@ type context struct { focus bool failureMode FailureMode + stackMode StackMode } // rootConvey is the main entry point to a test suite. This is called when @@ -101,6 +102,7 @@ func rootConvey(items ...interface{}) { focus: entry.Focus, failureMode: defaultFailureMode.combine(entry.FailMode), + stackMode: defaultStackMode.combine(entry.StackMode), } ctxMgr.SetValues(gls.Values{nodeKey: ctx}, func() { ctx.reporter.BeginStory(reporting.NewStoryReport(entry.Test)) @@ -154,6 +156,7 @@ func (ctx *context) Convey(items ...interface{}) { focus: entry.Focus, failureMode: ctx.failureMode.combine(entry.FailMode), + stackMode: ctx.stackMode.combine(entry.StackMode), } ctx.children[entry.Situation] = inner_ctx } @@ -173,7 +176,7 @@ func (ctx *context) So(actual interface{}, assert assertion, expected ...interfa if result := assert(actual, expected...); result == assertionSuccess { ctx.assertionReport(reporting.NewSuccessReport()) } else { - ctx.assertionReport(reporting.NewFailureReport(result)) + ctx.assertionReport(reporting.NewFailureReport(result, ctx.shouldShowStack())) } } @@ -206,6 +209,10 @@ func (c *context) shouldVisit() bool { return !c.complete && *c.expectChildRun } +func (c *context) shouldShowStack() bool { + return c.stackMode == StackFail +} + // 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. diff --git a/convey/discovery.go b/convey/discovery.go index eb8d4cb2..5e6d1f68 100644 --- a/convey/discovery.go +++ b/convey/discovery.go @@ -14,14 +14,16 @@ type suite struct { Focus bool Func func(C) // nil means skipped FailMode FailureMode + StackMode StackMode } -func newSuite(situation string, failureMode FailureMode, f func(C), test t, specifier actionSpecifier) *suite { +func newSuite(situation string, failureMode FailureMode, stackMode StackMode, f func(C), test t, specifier actionSpecifier) *suite { ret := &suite{ Situation: situation, Test: test, Func: f, FailMode: failureMode, + StackMode: stackMode, } switch specifier { case skipConvey: @@ -36,6 +38,7 @@ func discover(items []interface{}) *suite { name, items := parseName(items) test, items := parseGoTest(items) failure, items := parseFailureMode(items) + stack, items := parseStackMode(items) action, items := parseAction(items) specifier, items := parseSpecifier(items) @@ -43,7 +46,7 @@ func discover(items []interface{}) *suite { conveyPanic(parseError) } - return newSuite(name, failure, action, test, specifier) + return newSuite(name, failure, stack, action, test, specifier) } func item(items []interface{}) interface{} { if len(items) == 0 { @@ -70,6 +73,12 @@ func parseFailureMode(items []interface{}) (FailureMode, []interface{}) { } return FailureInherits, items } +func parseStackMode(items []interface{}) (StackMode, []interface{}) { + if mode, parsed := item(items).(StackMode); parsed { + return mode, items[1:] + } + return StackInherits, items +} func parseAction(items []interface{}) (func(C), []interface{}) { switch x := item(items).(type) { case nil: @@ -100,4 +109,4 @@ type t interface { Fail() } -const parseError = "You must provide a name (string), then a *testing.T (if in outermost scope), an optional FailureMode, and then an action (func())." +const parseError = "You must provide a name (string), then a *testing.T (if in outermost scope), an optional FailureMode and / or StackMode, and then an action (func())." diff --git a/convey/doc.go b/convey/doc.go index e4f7b51a..497dbcb2 100644 --- a/convey/doc.go +++ b/convey/doc.go @@ -135,6 +135,10 @@ func SkipSo(stuff ...interface{}) { // if their assertion fails. See constants further down for acceptable values type FailureMode string +// StackMode is a type which determines whether the So() blocks should report +// stack traces their assertion fails. See constants further down for acceptable values +type StackMode string + const ( // FailureContinues is a failure mode which prevents failing @@ -151,6 +155,19 @@ const ( // default to the failure-mode of the parent block. You should never // need to specify this mode in your tests.. FailureInherits FailureMode = "inherits" + + // StackError is a stack mode which tells Convey to print stack traces + // only for errors and not for test failures + StackError StackMode = "error" + + // StackFail is a stack mode which tells Convey to print stack traces + // for both errors and test failures + StackFail StackMode = "fail" + + // StackInherits is the default setting for stack-mode, it will + // default to the stack-mode of the parent block. You should never + // need to specify this mode in your tests.. + StackInherits StackMode = "inherits" ) func (f FailureMode) combine(other FailureMode) FailureMode { @@ -174,6 +191,27 @@ func SetDefaultFailureMode(mode FailureMode) { } } +func (s StackMode) combine(other StackMode) StackMode { + if other == StackInherits { + return s + } + return other +} + +var defaultStackMode StackMode = StackError + +// SetDefaultStackMode allows you to specify the default stack mode +// for all Convey blocks. It is meant to be used in an init function to +// allow the default mode to be changdd across all tests for an entire packgae +// but it can be used anywhere. +func SetDefaultStackMode(mode StackMode) { + if mode == StackError || mode == StackFail { + defaultStackMode = mode + } else { + panic("You may only use the constants named 'StackError' and 'StackFail' as default stack modes.") + } +} + //////////////////////////////////// Print functions //////////////////////////////////// // Print is analogous to fmt.Print (and it even calls fmt.Print). It ensures that diff --git a/convey/reporting/dot_test.go b/convey/reporting/dot_test.go index a8d20d46..2fe19ebd 100644 --- a/convey/reporting/dot_test.go +++ b/convey/reporting/dot_test.go @@ -12,7 +12,7 @@ func TestDotReporterAssertionPrinting(t *testing.T) { reporter := NewDotReporter(printer) reporter.Report(NewSuccessReport()) - reporter.Report(NewFailureReport("failed")) + reporter.Report(NewFailureReport("failed", false)) reporter.Report(NewErrorReport(errors.New("error"))) reporter.Report(NewSkipReport()) diff --git a/convey/reporting/gotest_test.go b/convey/reporting/gotest_test.go index fda18945..2d99f21e 100644 --- a/convey/reporting/gotest_test.go +++ b/convey/reporting/gotest_test.go @@ -17,7 +17,7 @@ func TestReporterReceivesFailureReport(t *testing.T) { reporter := NewGoTestReporter() test := new(fakeTest) reporter.BeginStory(NewStoryReport(test)) - reporter.Report(NewFailureReport("This is a failure.")) + reporter.Report(NewFailureReport("This is a failure.", false)) if !test.failed { t.Errorf("Test should have been marked as failed (but it wasn't).") diff --git a/convey/reporting/init.go b/convey/reporting/init.go index 99c3bd6d..976e8179 100644 --- a/convey/reporting/init.go +++ b/convey/reporting/init.go @@ -56,7 +56,8 @@ var ( dotError = "E" dotSkip = "S" errorTemplate = "* %s \nLine %d: - %v \n%s\n" - failureTemplate = "* %s \nLine %d:\n%s\n%s\n" + failureTemplate = "* %s \nLine %d:\n%s\n" + stackTemplate = "%s\n" ) var ( diff --git a/convey/reporting/problems.go b/convey/reporting/problems.go index 33d5e147..f119122d 100644 --- a/convey/reporting/problems.go +++ b/convey/reporting/problems.go @@ -53,7 +53,10 @@ func (self *problem) showFailures() { self.out.Println("\nFailures:\n") self.out.Indent() } - self.out.Println(failureTemplate, f.File, f.Line, f.Failure, f.StackTrace) + self.out.Println(failureTemplate, f.File, f.Line, f.Failure) + if f.StackTrace != "" { + self.out.Println(stackTemplate, f.StackTrace) + } } } diff --git a/convey/reporting/problems_test.go b/convey/reporting/problems_test.go index 92f0ca35..a28a6ff1 100644 --- a/convey/reporting/problems_test.go +++ b/convey/reporting/problems_test.go @@ -19,7 +19,7 @@ func TestNoopProblemReporterActions(t *testing.T) { func TestReporterPrintsFailuresAndErrorsAtTheEndOfTheStory(t *testing.T) { file, reporter := setup() - reporter.Report(NewFailureReport("failed")) + reporter.Report(NewFailureReport("failed", false)) reporter.Report(NewErrorReport("error")) reporter.Report(NewSuccessReport()) reporter.EndStory() diff --git a/convey/reporting/reports.go b/convey/reporting/reports.go index 712e6ade..f30789f6 100644 --- a/convey/reporting/reports.go +++ b/convey/reporting/reports.go @@ -97,10 +97,12 @@ type AssertionResult struct { Skipped bool } -func NewFailureReport(failure string) *AssertionResult { +func NewFailureReport(failure string, showStack bool) *AssertionResult { report := new(AssertionResult) report.File, report.Line = caller() - report.StackTrace = stackTrace() + if showStack { + report.StackTrace = stackTrace() + } parseFailure(failure, report) return report } diff --git a/convey/stack_trace_test.go b/convey/stack_trace_test.go new file mode 100644 index 00000000..71e1f53f --- /dev/null +++ b/convey/stack_trace_test.go @@ -0,0 +1,137 @@ +package convey + +import ( + "fmt" + "strings" + "testing" + + "github.com/smartystreets/goconvey/convey/reporting" +) + +func TestStackTrace(t *testing.T) { + file, test := setupFileReporter() + + Convey("A", test, func() { + So(1, ShouldEqual, 2) + }) + + if !strings.Contains(file.String(), "Failures:\n") { + t.Errorf("Expected errors, found none.") + } + if strings.Contains(file.String(), "goroutine ") { + t.Errorf("Found stack trace, expected none.") + } + + Convey("A", test, StackFail, func() { + So(1, ShouldEqual, 2) + }) + + if !strings.Contains(file.String(), "goroutine ") { + t.Errorf("Expected stack trace, found none.") + } +} + +func TestSetDefaultStackMode(t *testing.T) { + file, test := setupFileReporter() + SetDefaultStackMode(StackFail) // the default is normally StackError + defer SetDefaultStackMode(StackError) + + Convey("A", test, func() { + So(1, ShouldEqual, 2) + }) + + if !strings.Contains(file.String(), "goroutine ") { + t.Errorf("Expected stack trace, found none.") + } +} + +func TestStackModeMultipleInvocationInheritance(t *testing.T) { + file, test := setupFileReporter() + + // initial convey should default to StaskError, so no stack trace + Convey("A", test, FailureContinues, func() { + So(1, ShouldEqual, 2) + + // nested convey has explicit StaskFail, so should emit stack trace + Convey("B", StackFail, func() { + So(1, ShouldEqual, 2) + }) + }) + + stackCount := strings.Count(file.String(), "goroutine ") + if stackCount != 1 { + t.Errorf("Expected 1 stack trace, found %d.", stackCount) + fmt.Printf("RESULT: %s \n", file.String()) + } +} + +func TestStackModeMultipleInvocationInheritance2(t *testing.T) { + file, test := setupFileReporter() + + // Explicit StackFail, expect stack trace + Convey("A", test, FailureContinues, StackFail, func() { + So(1, ShouldEqual, 2) + + // Nested Convey inherits StackFail, expect stack trace + Convey("B", func() { + So(1, ShouldEqual, 2) + }) + }) + + stackCount := strings.Count(file.String(), "goroutine ") + if stackCount != 2 { + t.Errorf("Expected 2 stack traces, found %d.", stackCount) + } +} + +func TestStackModeMultipleInvocationInheritance3(t *testing.T) { + file, test := setupFileReporter() + + // Explicit StackFail, expect stack trace + Convey("A", test, FailureContinues, StackFail, func() { + So(1, ShouldEqual, 2) + + // Nested Convey explicitly sets StackError, so no stack trace + Convey("B", StackError, func() { + So(1, ShouldEqual, 2) + }) + }) + + stackCount := strings.Count(file.String(), "goroutine ") + if stackCount != 1 { + t.Errorf("Expected 1 stack trace1, found %d.", stackCount) + } +} + +func setupFileReporter() (*memoryFile, *fakeGoTest) { + //monochrome() + file := newMemoryFile() + printer := reporting.NewPrinter(file) + reporter := reporting.NewProblemReporter(printer) + testReporter = reporter + + return file, new(fakeGoTest) +} + +////////////////// memoryFile //////////////////// + +type memoryFile struct { + buffer string +} + +func (self *memoryFile) Write(p []byte) (n int, err error) { + self.buffer += string(p) + return len(p), nil +} + +func (self *memoryFile) String() string { + return self.buffer +} + +func newMemoryFile() *memoryFile { + return new(memoryFile) +} + +// func monochrome() { +// greenColor, yellowColor, redColor, resetColor = "", "", "", "" +// } diff --git a/examples/stack_test.go b/examples/stack_test.go new file mode 100644 index 00000000..8cdf5537 --- /dev/null +++ b/examples/stack_test.go @@ -0,0 +1,15 @@ +package examples + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestStackFail(t *testing.T) { + t.Parallel() + + Convey("Given a test that will fail", t, StackFail, func() { + So(1, ShouldEqual, 2) + }) +}