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 stacktrace to chain and formatted data #396

Merged
merged 4 commits into from
Jun 2, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions assertion.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ type AssertionFailure struct {

// Allowed delta between actual and expected
Delta *AssertionValue

// Stacktrace of a fail
Stacktrace []CallerInfo
}

// AssertionValue holds expected or actual value
Expand Down
42 changes: 42 additions & 0 deletions chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package httpexpect

import (
"fmt"
"runtime"
"sync"
"testing"

Expand Down Expand Up @@ -435,6 +436,9 @@ func (c *chain) fail(failure AssertionFailure) {
if c.severity == SeverityError {
failure.IsFatal = true
}

failure.Stacktrace = GetCallerInfo()

c.failure = &failure
}

Expand Down Expand Up @@ -502,3 +506,41 @@ func isTestingTB(in AssertionHandler) bool {
}
return false
}

type CallerInfo struct {
FuncName string
File string
Line int
}

func GetCallerInfo() []CallerInfo {
callers := []CallerInfo{}
for i := 0; ; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}

if file == "<autogenerated>" {
break
}

f := runtime.FuncForPC(pc)
if f == nil {
break
}
funcName := f.Name()

if funcName == "testing.tRunner" {
break
}

callers = append(callers, CallerInfo{
FuncName: funcName,
File: file,
Line: line,
})
}

return callers
}
18 changes: 18 additions & 0 deletions chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,22 @@ func TestChain_Severity(t *testing.T) {
})
}

func TestChain_Stacktrace(t *testing.T) {
handler := &mockAssertionHandler{}

chain := newChainWithConfig("test", Config{
AssertionHandler: handler,
}.withDefaults())

opChain := chain.enter("test")
opChain.fail(testFailure())
opChain.leave()

assert.True(t, opChain.failed())
assert.NotNil(t, handler.failure)
assert.NotEmpty(t, handler.failure.Stacktrace)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do some simple sanity check here?

E.g. that Stacktrace contains "TestChain_Stacktrace" and "chain.fail".

}

func TestChain_Reporting(t *testing.T) {
handler := &mockAssertionHandler{}

Expand Down Expand Up @@ -708,6 +724,8 @@ func TestChain_Reporting(t *testing.T) {
assert.True(t, chain.failed()) // reported to parent
assert.NotNil(t, handler.ctx) // reported to handler
assert.NotNil(t, handler.failure)

failure.Stacktrace = handler.failure.Stacktrace
assert.Equal(t, failure, *handler.failure)
}

Expand Down
41 changes: 41 additions & 0 deletions formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type DefaultFormatter struct {
// Exclude HTTP response from failure report.
DisableResponses bool

// Enables printing of stacktrace on failure
StacktraceMode StacktraceMode

// Thousand separator.
// Default is DigitSeparatorUnderscore.
DigitSeparator DigitSeparator
Expand Down Expand Up @@ -112,6 +115,16 @@ func (f *DefaultFormatter) FormatFailure(
}
}

type StacktraceMode int

const (
// Unconditionally disable stacktrace.
StacktraceModeDisabled StacktraceMode = iota

// Format caller info as `at [function name]([file]:[line])`
StacktraceModeDefault
)

// DigitSeparator defines the separator used to format integers and floats.
type DigitSeparator int

Expand Down Expand Up @@ -209,6 +222,9 @@ type FormatData struct {
HaveResponse bool
Response string

HaveStacktrace bool
Stacktrace []string

EnableColors bool
LineWidth int
}
Expand Down Expand Up @@ -284,6 +300,7 @@ func (f *DefaultFormatter) buildFormatData(

f.fillRequest(&data, ctx, failure)
f.fillResponse(&data, ctx, failure)
f.fillStacktrace(&data, ctx, failure)
}

return &data
Expand Down Expand Up @@ -546,6 +563,23 @@ func (f *DefaultFormatter) fillResponse(
}
}

func (f *DefaultFormatter) fillStacktrace(
data *FormatData, ctx *AssertionContext, failure *AssertionFailure,
) {
data.Stacktrace = []string{}

if f.StacktraceMode == StacktraceModeDisabled {
return
}
if f.StacktraceMode == StacktraceModeDefault {
for _, call := range failure.Stacktrace {
formatted := fmt.Sprintf("at %s(%s:%d)", call.FuncName, call.File, call.Line)
data.Stacktrace = append(data.Stacktrace, formatted)
}
data.HaveStacktrace = len(failure.Stacktrace) != 0
}
}

func (f *DefaultFormatter) formatValue(value interface{}) string {
if flt := extractFloat32(value); flt != nil {
return f.reformatNumber(f.formatFloatValue(*flt, 32))
Expand Down Expand Up @@ -1016,6 +1050,13 @@ request: {{ .Request | indent | trim | color $.EnableColors "HiMagenta" }}

response: {{ .Response | indent | trim | color $.EnableColors "HiMagenta" }}
{{- end -}}
{{- if .HaveStacktrace }}

trace:
{{- range $n, $call := .Stacktrace }}
{{ $call | indent }}
{{- end -}}
{{- end -}}
{{- if .AssertPath }}

assertion:
Expand Down
45 changes: 45 additions & 0 deletions formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,48 @@ func TestFormatter_ColorMode(t *testing.T) {
})
}
}

func TestFormatter_Stacktrace(t *testing.T) {
df := &DefaultFormatter{
StacktraceMode: StacktraceModeDefault,
}
ctx := &AssertionContext{}

cases := []struct {
callerInfo CallerInfo
want string
}{
{
CallerInfo{
FuncName: "Foo()",
File: "formatter_test.go",
Line: 228,
},
"at Foo()(formatter_test.go:228)",
},
{
CallerInfo{
FuncName: "Bar()",
File: "formatter.go",
Line: 123,
},
"at Bar()(formatter.go:123)",
},
{
CallerInfo{
FuncName: "Buzz()",
File: "file.go",
Line: 5,
},
"at Buzz()(file.go:5)",
},
}

for _, tc := range cases {
fl := &AssertionFailure{
Stacktrace: []CallerInfo{tc.callerInfo},
}
fd := df.buildFormatData(ctx, fl)
assert.Equal(t, []string{tc.want}, fd.Stacktrace)
}
}