Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add StackMode to make stack traces configurable #588

Merged
merged 1 commit into from Oct 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion convey/context.go
Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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
}
Expand All @@ -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()))
}
}

Expand Down Expand Up @@ -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.
Expand Down
15 changes: 12 additions & 3 deletions convey/discovery.go
Expand Up @@ -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:
Expand All @@ -36,14 +38,15 @@ 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)

if len(items) != 0 {
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 {
Expand All @@ -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:
Expand Down Expand Up @@ -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())."
40 changes: 39 additions & 1 deletion convey/doc.go
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -164,7 +181,7 @@ var defaultFailureMode FailureMode = FailureHalts

// SetDefaultFailureMode allows you to specify the default failure 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
// allow the default mode to be changed across all tests for an entire packgae
// but it can be used anywhere.
func SetDefaultFailureMode(mode FailureMode) {
if mode == FailureContinues || mode == FailureHalts {
Expand All @@ -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 changed 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
Expand Down
2 changes: 1 addition & 1 deletion convey/reporting/dot_test.go
Expand Up @@ -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())

Expand Down
2 changes: 1 addition & 1 deletion convey/reporting/gotest_test.go
Expand Up @@ -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).")
Expand Down
3 changes: 2 additions & 1 deletion convey/reporting/init.go
Expand Up @@ -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 (
Expand Down
5 changes: 4 additions & 1 deletion convey/reporting/problems.go
Expand Up @@ -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)
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion convey/reporting/problems_test.go
Expand Up @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions convey/reporting/reports.go
Expand Up @@ -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
}
Expand Down
137 changes: 137 additions & 0 deletions 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 = "", "", "", ""
// }