From e77d4004ffcf2ed90956b46dcdeb4a05e2fad4af Mon Sep 17 00:00:00 2001 From: Thomas Miller Date: Fri, 8 Sep 2023 14:20:27 +0100 Subject: [PATCH] Modifies tracing to removed Cause based errors. This work changes tracing to now offer a new Locationer based error into stack. Printing an Error stack now will provide a line for each error in the stack by calling Unwrap() till no more errors are found. This fundementally changes the output produced by ErrorStack to now just include each call to Trace() but also contain the value of the error at each Trace() call. This PR also provides a new documentation worked example. --- example_test.go | 37 +++++++++--- functions.go | 141 ++++++++++++++++++---------------------------- functions_test.go | 44 ++++++--------- 3 files changed, 99 insertions(+), 123 deletions(-) diff --git a/example_test.go b/example_test.go index 2a79cf48..001926ce 100644 --- a/example_test.go +++ b/example_test.go @@ -1,4 +1,4 @@ -// Copyright 2013, 2014 Canonical Ltd. +// Copyright 2023 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package errors_test @@ -10,14 +10,33 @@ import ( ) func ExampleTrace() { - var err1 error = fmt.Errorf("something wicked this way comes") - var err2 error = nil + err := fmt.Errorf("Too many gophers to count") + tracedErr := errors.Trace(err) - // Tracing a non nil error will return an error - fmt.Println(errors.Trace(err1)) - // Tracing nil will return nil - fmt.Println(errors.Trace(err2)) + fmt.Println(tracedErr) + fmt.Println(errors.Is(tracedErr, err)) + fmt.Println(errors.ErrorStack(tracedErr)) + fmt.Println() - // Output: something wicked this way comes - // + tracedErr = errors.Trace(tracedErr) + fmt.Println(errors.ErrorStack(tracedErr)) + fmt.Println() + + tracedErr = errors.Trace(fmt.Errorf("foobar: %w", tracedErr)) + fmt.Println(errors.ErrorStack(tracedErr)) + + // Output: Too many gophers to count + // true + // Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:14: Too many gophers to count + // + // Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:14: Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:21: Too many gophers to count + // + // Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:14: Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:21: Too many gophers to count + // foobar: Too many gophers to count + // github.com/juju/errors_test.ExampleTrace:25: foobar: Too many gophers to count } diff --git a/functions.go b/functions.go index 952a6739..f5dc26a6 100644 --- a/functions.go +++ b/functions.go @@ -14,8 +14,8 @@ import ( // the location that the error is created. // // For example: -// return errors.New("validation failed") // +// return errors.New("validation failed") func New(message string) error { err := &Err{message: message} err.SetLocation(1) @@ -26,8 +26,8 @@ func New(message string) error { // error is created. This should be a drop in replacement for fmt.Errorf. // // For example: -// return errors.Errorf("validation failed: %s", message) // +// return errors.Errorf("validation failed: %s", message) func Errorf(format string, args ...interface{}) error { err := &Err{message: fmt.Sprintf(format, args...)} err.SetLocation(1) @@ -46,23 +46,11 @@ func getLocation(callDepth int) (string, int) { return frame.Function, frame.Line } -// Trace adds the location of the Trace call to the stack. The Cause of the -// resulting error is the same as the error parameter. If the other error is -// nil, the result will be nil. -// -// For example: -// if err := SomeFunc(); err != nil { -// return errors.Trace(err) -// } -// +// Trace adds the location that the error was traced at. The returned error +// satisfies the Locationer interface and will unwrap to the passed in error. If +// error is nil then nil will be returned. func Trace(other error) error { - //return SetLocation(other, 2) - if other == nil { - return nil - } - err := &Err{previous: other, cause: Cause(other)} - err.SetLocation(1) - return err + return SetLocation(other, 2) } // Annotate is used to add extra context to an existing error. The location of @@ -70,10 +58,10 @@ func Trace(other error) error { // function are also recorded. // // For example: -// if err := SomeFunc(); err != nil { -// return errors.Annotate(err, "failed to frombulate") -// } // +// if err := SomeFunc(); err != nil { +// return errors.Annotate(err, "failed to frombulate") +// } func Annotate(other error, message string) error { if other == nil { return nil @@ -92,10 +80,10 @@ func Annotate(other error, message string) error { // function are also recorded. // // For example: -// if err := SomeFunc(); err != nil { -// return errors.Annotatef(err, "failed to frombulate the %s", arg) -// } // +// if err := SomeFunc(); err != nil { +// return errors.Annotatef(err, "failed to frombulate the %s", arg) +// } func Annotatef(other error, format string, args ...interface{}) error { if other == nil { return nil @@ -116,8 +104,7 @@ func Annotatef(other error, format string, args ...interface{}) error { // // For example: // -// defer DeferredAnnotatef(&err, "failed to frombulate the %s", arg) -// +// defer DeferredAnnotatef(&err, "failed to frombulate the %s", arg) func DeferredAnnotatef(err *error, format string, args ...interface{}) { if *err == nil { return @@ -135,11 +122,11 @@ func DeferredAnnotatef(err *error, format string, args ...interface{}) { // stored in the error stack. // // For example: -// if err := SomeFunc(); err != nil { -// newErr := &packageError{"more context", private_value} -// return errors.Wrap(err, newErr) -// } // +// if err := SomeFunc(); err != nil { +// newErr := &packageError{"more context", private_value} +// return errors.Wrap(err, newErr) +// } func Wrap(other, newDescriptive error) error { err := &Err{ previous: other, @@ -149,14 +136,14 @@ func Wrap(other, newDescriptive error) error { return err } -// Wrapf changes the Cause of the error, and adds an annotation. The location -// of the Wrap call is also stored in the error stack. +// Deprecated: Wrapf changes the Cause of the error, and adds an annotation. The +// location of the Wrap call is also stored in the error stack. // // For example: -// if err := SomeFunc(); err != nil { -// return errors.Wrapf(err, simpleErrorType, "invalid value %q", value) -// } // +// if err := SomeFunc(); err != nil { +// return errors.Wrapf(err, simpleErrorType, "invalid value %q", value) +// } func Wrapf(other, newDescriptive error, format string, args ...interface{}) error { err := &Err{ message: fmt.Sprintf(format, args...), @@ -167,10 +154,10 @@ func Wrapf(other, newDescriptive error, format string, args ...interface{}) erro return err } -// Maskf masks the given error with the given format string and arguments (like -// fmt.Sprintf), returning a new error that maintains the error stack, but -// hides the underlying error type. The error string still contains the full -// annotations. If you want to hide the annotations, call Wrap. +// Deprecated: Maskf masks the given error with the given format string and +// arguments (like fmt.Sprintf), returning a new error that maintains the error +// stack, but hides the underlying error type. The error string still contains +// the full annotations. If you want to hide the annotations, call Wrap. func Maskf(other error, format string, args ...interface{}) error { if other == nil { return nil @@ -184,6 +171,7 @@ func Maskf(other error, format string, args ...interface{}) error { } // Mask hides the underlying error type, and records the location of the masking. +// Deprecated: no replacement provided. func Mask(other error) error { if other == nil { return nil @@ -235,7 +223,7 @@ var ( // Details returns information about the stack of errors wrapped by err, in // the format: // -// [{filename:99: error one} {otherfile:55: cause of error one}] +// [{filename:99: error one} {otherfile:55: cause of error one}] // // This is a terse alternative to ErrorStack as it returns a single line. func Details(err error) string { @@ -270,24 +258,22 @@ func Details(err error) string { return string(s) } -// ErrorStack returns a string representation of the annotated error. If the -// error passed as the parameter is not an annotated error, the result is -// simply the result of the Error() method on that error. +// ErrorStack returns a string representation of all the errors located in the +// stack pointed at by err. If an error in the stack satisfies the Locationer +// interface then the filename and line number of the location will be printed +// in the stack. Each line in the output represents an Unwrap() of the error. // -// If the error is an annotated error, a multi-line string is returned where -// each line represents one entry in the annotation stack. The full filename -// from the call stack is used in the output. -// -// first error -// github.com/juju/errors/annotation_test.go:193: -// github.com/juju/errors/annotation_test.go:194: annotation -// github.com/juju/errors/annotation_test.go:195: -// github.com/juju/errors/annotation_test.go:196: more context -// github.com/juju/errors/annotation_test.go:197: +// Example: +// github.com/juju/errors_test.ExampleTrace:14: Too many gophers to count +// github.com/juju/errors_test.ExampleTrace:21: Too many gophers to count +// foobar: Too many gophers to count +// github.com/juju/errors_test.ExampleTrace:25: foobar: Too many gophers to count func ErrorStack(err error) string { return strings.Join(errorStack(err), "\n") } +// errorStack returns a slice of strings representing each error found in the +// stack by recursively calling Unwrap(). func errorStack(err error) []string { if err == nil { return nil @@ -296,36 +282,17 @@ func errorStack(err error) []string { // We want the first error first var lines []string for { - var buff []byte + out := strings.Builder{} if err, ok := err.(Locationer); ok { file, line := err.Location() // Strip off the leading GOPATH/src path elements. if file != "" { - buff = append(buff, fmt.Sprintf("%s:%d", file, line)...) - buff = append(buff, ": "...) + fmt.Fprintf(&out, "%s:%d: ", file, line) } } - if cerr, ok := err.(wrapper); ok { - message := cerr.Message() - buff = append(buff, message...) - // If there is a cause for this error, and it is different to the cause - // of the underlying error, then output the error string in the stack trace. - var cause error - if err1, ok := err.(causer); ok { - cause = err1.Cause() - } - err = cerr.Underlying() - if cause != nil && !sameError(Cause(err), cause) { - if message != "" { - buff = append(buff, ": "...) - } - buff = append(buff, cause.Error()...) - } - } else { - buff = append(buff, err.Error()...) - err = nil - } - lines = append(lines, string(buff)) + fmt.Fprintf(&out, err.Error()) + err = stderrors.Unwrap(err) + lines = append(lines, out.String()) if err == nil { break } @@ -354,11 +321,11 @@ func Is(err, target error) bool { // HasType is a function wrapper around AsType dropping the where return value // from AsType() making a function that can be used like this: // -// return HasType[*MyError](err) +// return HasType[*MyError](err) // // Or // -// if HasType[*MyError](err) {} +// if HasType[*MyError](err) {} func HasType[T error](err error) bool { _, rval := AsType[T](err) return rval @@ -383,16 +350,16 @@ func As(err error, target interface{}) bool { // the target, to avoid having to define a variable before the call. For // example, callers can replace this: // -// var pathError *fs.PathError -// if errors.As(err, &pathError) { -// fmt.Println("Failed at path:", pathError.Path) -// } +// var pathError *fs.PathError +// if errors.As(err, &pathError) { +// fmt.Println("Failed at path:", pathError.Path) +// } // // With: // -// if pathError, ok := errors.AsType[*fs.PathError](err); ok { -// fmt.Println("Failed at path:", pathError.Path) -// } +// if pathError, ok := errors.AsType[*fs.PathError](err); ok { +// fmt.Println("Failed at path:", pathError.Path) +// } func AsType[T error](err error) (T, bool) { for err != nil { if e, is := err.(T); is { @@ -450,5 +417,5 @@ func Hide(err error) error { if err == nil { return nil } - return &fmtNoop{err} + return &fmtNoop{SetLocation(err, 3)} } diff --git a/functions_test.go b/functions_test.go index 9f1a11aa..a570c8c2 100644 --- a/functions_test.go +++ b/functions_test.go @@ -66,7 +66,7 @@ func (*functionSuite) TestAnnotate(c *gc.C) { func (*functionSuite) TestAnnotatef(c *gc.C) { first := errors.New("first") - err := errors.Annotatef(first, "annotation %d", 2) //err annotatefTest + err := errors.Annotatef(first, "annotation %d", 2) // err annotatefTest loc := errorLocationValue(c) c.Assert(err.Error(), gc.Equals, "annotation 2: first") @@ -189,10 +189,6 @@ func (*functionSuite) TestCause(c *gc.C) { c.Assert(os.IsNotExist(errors.Cause(err)), gc.Equals, true) } -type tracer interface { - StackTrace() []string -} - func (*functionSuite) TestErrorStack(c *gc.C) { for i, test := range []struct { message string @@ -223,7 +219,7 @@ func (*functionSuite) TestErrorStack(c *gc.C) { err := errors.New("first error") fmt.Fprintf(expected, "%s: first error\n", errorLocationValue(c)) err = errors.Annotate(err, "annotation") - fmt.Fprintf(expected, "%s: annotation", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: annotation: first error", errorLocationValue(c)) return err }, tracer: true, @@ -245,7 +241,7 @@ func (*functionSuite) TestErrorStack(c *gc.C) { err = errors.Wrap(err, fmt.Errorf("detailed error")) fmt.Fprintf(expected, "%s: detailed error\n", errorLocationValue(c)) err = errors.Annotatef(err, "annotated") - fmt.Fprintf(expected, "%s: annotated", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: annotated: detailed error", errorLocationValue(c)) return err }, tracer: true, @@ -255,15 +251,15 @@ func (*functionSuite) TestErrorStack(c *gc.C) { err := errors.New("first error") fmt.Fprintf(expected, "%s: first error\n", errorLocationValue(c)) err = errors.Trace(err) - fmt.Fprintf(expected, "%s: \n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: first error\n", errorLocationValue(c)) err = errors.Annotate(err, "some context") - fmt.Fprintf(expected, "%s: some context\n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: some context: first error\n", errorLocationValue(c)) err = errors.Trace(err) - fmt.Fprintf(expected, "%s: \n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: some context: first error\n", errorLocationValue(c)) err = errors.Annotate(err, "more context") - fmt.Fprintf(expected, "%s: more context\n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: more context: some context: first error\n", errorLocationValue(c)) err = errors.Trace(err) - fmt.Fprintf(expected, "%s: ", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: more context: some context: first error", errorLocationValue(c)) return err }, tracer: true, @@ -273,15 +269,15 @@ func (*functionSuite) TestErrorStack(c *gc.C) { err := newNonComparableError("first error") fmt.Fprintln(expected, "first error") err = errors.Trace(err) - fmt.Fprintf(expected, "%s: \n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: first error\n", errorLocationValue(c)) err = errors.Wrap(err, newError("value error")) fmt.Fprintf(expected, "%s: value error\n", errorLocationValue(c)) err = errors.Maskf(err, "masked") - fmt.Fprintf(expected, "%s: masked\n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: masked: value error\n", errorLocationValue(c)) err = errors.Annotate(err, "more context") - fmt.Fprintf(expected, "%s: more context\n", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: more context: masked: value error\n", errorLocationValue(c)) err = errors.Trace(err) - fmt.Fprintf(expected, "%s: ", errorLocationValue(c)) + fmt.Fprintf(expected, "%s: more context: masked: value error", errorLocationValue(c)) return err }, tracer: true, @@ -294,21 +290,13 @@ func (*functionSuite) TestErrorStack(c *gc.C) { if !ok { c.Logf("%#v", err) } - tracer, ok := err.(tracer) - c.Check(ok, gc.Equals, test.tracer) - if ok { - stackTrace := tracer.StackTrace() - c.Check(stackTrace, gc.DeepEquals, strings.Split(stack, "\n")) - } } } func (*functionSuite) TestFormat(c *gc.C) { formatErrorExpected := &strings.Builder{} err := errors.New("TestFormat") - fmt.Fprintf(formatErrorExpected, "%s: TestFormat\n", errorLocationValue(c)) - err = errors.Mask(err) - fmt.Fprintf(formatErrorExpected, "%s: ", errorLocationValue(c)) + fmt.Fprintf(formatErrorExpected, "%s: TestFormat", errorLocationValue(c)) for i, test := range []struct { format string @@ -380,13 +368,15 @@ func (*functionSuite) TestSetLocationWithNilError(c *gc.C) { } func (*functionSuite) TestSetLocation(c *gc.C) { + builder := strings.Builder{} err := errors.New("test") + fmt.Fprintf(&builder, "%s: test\n", errorLocationValue(c)) err = errors.SetLocation(err, 1) - stack := fmt.Sprintf("%s: test", errorLocationValue(c)) + fmt.Fprintf(&builder, "%s: test", errorLocationValue(c)) _, implements := err.(errors.Locationer) c.Assert(implements, gc.Equals, true) - c.Check(errors.ErrorStack(err), gc.Equals, stack) + c.Check(errors.ErrorStack(err), gc.Equals, builder.String()) } func (*functionSuite) TestHideErrorStillReturnsErrorString(c *gc.C) {