Skip to content

Commit

Permalink
Add TraceCaller interface for extensibility
Browse files Browse the repository at this point in the history
Signed-off-by: Steve Coffman <steve@khanacademy.org>
  • Loading branch information
StevenACoffman committed May 5, 2024
1 parent dc78660 commit da1e9e9
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 4 deletions.
12 changes: 12 additions & 0 deletions errtrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,15 @@ func (e *errTrace) Format(s fmt.State, verb rune) {

fmt.Fprintf(s, fmt.FormatString(s, verb), e.err)
}

// TraceCall returns the program counter for the location in this frame.
func (e *errTrace) TraceCall() uintptr {
return e.pc
}

type TraceCaller interface {

Check warning on line 127 in errtrace.go

View workflow job for this annotation

GitHub Actions / Lint

exported: exported type TraceCaller should have comment or be unexported (revive)
TraceCall() uintptr
}

// compile time TraceCaller interface check
var _ TraceCaller = &errTrace{}
26 changes: 22 additions & 4 deletions unwrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,36 @@ import "runtime"
//
// You can use this for structured access to trace information.
func UnwrapFrame(err error) (frame runtime.Frame, inner error, ok bool) { //nolint:revive // error is intentionally middle return
e, ok := err.(*errTrace)
e, ok := err.(TraceCaller)
if !ok {
return runtime.Frame{}, err, false
}

frames := runtime.CallersFrames([]uintptr{e.pc})
wrapErr := UnwrapOnce(err)
frames := runtime.CallersFrames([]uintptr{e.TraceCall()})
f, _ := frames.Next()
if f == (runtime.Frame{}) {
// Unlikely, but if PC didn't yield a frame,
// just return the inner error.
return runtime.Frame{}, e.err, false
return runtime.Frame{}, wrapErr, false
}

return f, e.err, true
return f, wrapErr, true
}

// UnwrapOnce accesses the direct cause of the error if any, otherwise
// returns nil.
//
// It supports both errors implementing causer (`Cause()` method, from
// github.com/pkg/errors) and `Wrapper` (`Unwrap()` method, from the
// Go 2 error proposal).
func UnwrapOnce(err error) error {
switch e := err.(type) {
case interface{ Cause() error }:
return e.Cause()

Check warning on line 38 in unwrap.go

View check run for this annotation

Codecov / codecov/patch

unwrap.go#L37-L38

Added lines #L37 - L38 were not covered by tests
case interface{ Unwrap() error }:
return e.Unwrap()
}

return nil
}
49 changes: 49 additions & 0 deletions unwrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,52 @@ func TestUnwrapFrame_badPC(t *testing.T) {
t.Errorf("inner: got %v, want %v", inner, giveErr)
}
}

func TestUnwrapOnce(t *testing.T) {
rootErr := New("root")
var errTrace *errTrace
errors.As(rootErr, &errTrace)
unwrapped := errTrace.err
wrapper := Wrap(rootErr)

type want struct {
wantErr bool
matchErr error
matchString string
}
tests := []struct {
name string
arg error
want want
}{
{
name: "unwrap wrapped provides root",
arg: wrapper,
want: want{
wantErr: true,
matchErr: rootErr,
matchString: "root",
},
},
{
name: "unwrap root provides unwrapped",
arg: rootErr,
want: want{
wantErr: true,
matchErr: unwrapped,
matchString: "root",
},
},
{name: "unwrap nil provides nil"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := UnwrapOnce(tt.arg)
if (err != nil) != tt.want.wantErr {
t.Errorf("UnwrapOnce() error = %v, but wantErr %v", err, tt.want.wantErr)
} else if !errors.Is(err, tt.want.matchErr) {
t.Errorf("UnwrapOnce() error = %v, does not match matchErr %v", err, tt.want.matchErr)
}
})
}
}

0 comments on commit da1e9e9

Please sign in to comment.