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 TraceCaller interface for extensibility for #104 #106

Merged
merged 9 commits into from
May 6, 2024
23 changes: 22 additions & 1 deletion errtrace.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func wrap(err error, callerPC uintptr) error {
}

// Format writes the return trace for given error to the writer.
// The output takes a fromat similar to the following:
// The output takes a format similar to the following:
//
// <error message>
//
Expand All @@ -79,6 +79,8 @@ func wrap(err error, callerPC uintptr) error {
// <file>:<line>
// [...]
//
// Any error that has a method `TracePC() uintptr` will
// contribute to the trace.
// If the error doesn't have a return trace attached to it,
// only the error message is reported.
// If the error is comprised of multiple errors (e.g. with [errors.Join]),
Expand All @@ -90,6 +92,8 @@ func Format(w io.Writer, target error) (err error) {
}

// FormatString writes the return trace for err to a string.
// Any error that has a method `TracePC() uintptr` will
// contribute to the trace.
// See [Format] for details of the output format.
func FormatString(target error) string {
var s strings.Builder
Expand Down Expand Up @@ -118,3 +122,20 @@ func (e *errTrace) Format(s fmt.State, verb rune) {

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

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

// tracePCprovider is a provider of the Program Counter
// that the error originated with.
// The returned PC is intended to be used with
// runtime.CallersFrames or runtime.FuncForPC
// to aid in generating the error return trace
type tracePCprovider interface {
StevenACoffman marked this conversation as resolved.
Show resolved Hide resolved
TracePC() uintptr
}

// compile time tracePCprovider interface check
var _ tracePCprovider = &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.(interface{ TracePC() uintptr })
if !ok {
return runtime.Frame{}, err, false
}

frames := runtime.CallersFrames([]uintptr{e.pc})
wrapErr := unwrapOnce(err)
StevenACoffman marked this conversation as resolved.
Show resolved Hide resolved
frames := runtime.CallersFrames([]uintptr{e.TracePC()})
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).
StevenACoffman marked this conversation as resolved.
Show resolved Hide resolved
func unwrapOnce(err error) error {
switch e := err.(type) {
case interface{ Cause() error }:
return e.Cause()
case interface{ Unwrap() error }:
return e.Unwrap()
}

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

// caused follows the pkg/errors Cause interface
type caused struct {
err error
}

func (e *caused) Error() string {
return e.err.Error()
}

func (e *caused) Cause() error {
return e.err
}

func TestUnwrapOnce(t *testing.T) {
StevenACoffman marked this conversation as resolved.
Show resolved Hide resolved
rootErr := New("root")
var errTrace *errTrace
errors.As(rootErr, &errTrace)
unwrapped := errTrace.err
wrapper := Wrap(rootErr)

causedErr := &caused{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"},
{
name: "unwrap caused provides root",
arg: causedErr,
want: want{
wantErr: true,
matchErr: rootErr,
matchString: "root",
},
},
}
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)
}
})
}
}