Skip to content

Commit

Permalink
implement Exception.Unwrap method
Browse files Browse the repository at this point in the history
In order to make exceptions testable with errors.Is, they must implement
the `Unwrap` method to allow access to the underlying error code(s).

This change is addressing the problem of the below code not working as
expected, when testing returned by `Do` method error:

    c, _ := ch.Dial(ctx, ch.Options{...})
    switch err := c.Do(ctx, ch.Query{...}); {
    case err == nil:
    	// ...
    case errors.Is(err, proto.ErrTableIsReadOnly), errors.Is(err, proto.ErrReadonly):
    	// ... <- Never executed, because exception unwraping is not implemented.
    default:
        // ...
    }

Because `Exception` instances are created dynamically, unwrapping can
ignore nested exception instances and collect only the underlying error
codes.
  • Loading branch information
husio committed Oct 10, 2023
1 parent cce1c00 commit 201ad9b
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 0 deletions.
21 changes: 21 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,27 @@ func (e *Exception) Error() string {
return fmt.Sprintf("%s: %s: %s", e.Code, e.Name, msg)
}

// Unwrap implements errors.Unwrap interface.
func (e *Exception) Unwrap() []error {
if e == nil {
return nil
}
codes := make([]error, 0, 12)
// Flatten the error tree by collecting all error codes. Only error codes
// matter because they are declared and therefore can be compared using
// errors.Is. Dynamically created Exception instances are not relevant for
// this functionality.
return collectCodes(e, codes)
}

func collectCodes(e *Exception, codes []error) []error {
codes = append(codes, e.Code)
for _, next := range e.Next {
codes = collectCodes(&next, codes)

Check failure on line 150 in client.go

View workflow job for this annotation

GitHub Actions / lint / run

G601: Implicit memory aliasing in for loop. (gosec)
}
return codes
}

// AsException finds first *Exception in err chain.
func AsException(err error) (*Exception, bool) {
var e *Exception
Expand Down
28 changes: 28 additions & 0 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ch

import (
"context"
"errors"
"os"
"testing"

Expand Down Expand Up @@ -87,3 +88,30 @@ func TestDial(t *testing.T) {
require.True(t, IsErr(err, proto.ErrUnknownDatabase))
})
}

func TestExceptionUnwrap(t *testing.T) {
flat := &Exception{
Code: proto.ErrReadonly,
Name: "foo",
Message: "bar",
Next: nil,
}

if !errors.Is(flat, proto.ErrReadonly) {
t.Fatal("flat exception must be the error code it represents")
}

nested := &Exception{
Code: proto.ErrAborted,
Name: "foo",
Message: "bar",
Next: []Exception{*flat},
}
if !errors.Is(nested, proto.ErrAborted) {
t.Fatal("nested exception must be the error code it represents")
}
if !errors.Is(nested, proto.ErrReadonly) {
t.Fatal("nested exception must be the error code it wraps")
}

Check failure on line 116 in client_test.go

View workflow job for this annotation

GitHub Actions / lint / run

unnecessary trailing newline (whitespace)
}

0 comments on commit 201ad9b

Please sign in to comment.