Skip to content

Commit

Permalink
gcs: update error types.
Browse files Browse the repository at this point in the history
This updates the gcs error types to leverage go 1.13 errors.Is/As functionality as well as confirm to the error infrastructure best practices.
  • Loading branch information
dnldd authored and davecgh committed Nov 11, 2020
1 parent 5bf4099 commit e092918
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 98 deletions.
5 changes: 3 additions & 2 deletions gcs/bits_test.go
Expand Up @@ -7,6 +7,7 @@ package gcs
import (
"bytes"
"encoding/hex"
"errors"
"io"
"testing"
)
Expand Down Expand Up @@ -340,7 +341,7 @@ nextTest:
// Read unary and ensure expected result if requested.
if prTest.doUnary {
gotUnary, err := r.readUnary()
if err != prTest.unaryErr {
if !errors.Is(err, prTest.unaryErr) {
t.Errorf("%q-%q: unexpected unary err -- got %v, want %v",
test.name, prTest.name, err, prTest.unaryErr)
continue nextTest
Expand All @@ -359,7 +360,7 @@ nextTest:
// Read specified number of bits as uint64 and ensure expected
// result.
gotVal, err := r.readNBits(prTest.nValBits)
if err != prTest.bitsErr {
if !errors.Is(err, prTest.bitsErr) {
t.Errorf("%q-%q: unexpected nbits err -- got %v, want %v",
test.name, prTest.name, err, prTest.bitsErr)
continue nextTest
Expand Down
64 changes: 23 additions & 41 deletions gcs/error.go
@@ -1,74 +1,56 @@
// Copyright (c) 2019 The Decred developers
// Copyright (c) 2020 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package gcs

import (
"fmt"
)

// ErrorCode identifies a kind of error.
type ErrorCode int
// ErrorKind identifies a kind of error. It has full support for errors.Is and
// errors.As, so the caller can directly check against an error kind when
// determining the reason for an error.
type ErrorKind string

// These constants are used to identify a specific RuleError.
const (
// ErrNTooBig signifies that the filter can't handle N items.
ErrNTooBig ErrorCode = iota
ErrNTooBig = ErrorKind("ErrNTooBig")

// ErrPTooBig signifies that the filter can't handle `1/2**P`
// collision probability.
ErrPTooBig
ErrPTooBig = ErrorKind("ErrPTooBig")

// ErrBTooBig signifies that the provided Golomb coding bin size is larger
// than the maximum allowed value.
ErrBTooBig
ErrBTooBig = ErrorKind("ErrBTooBig")

// ErrMisserialized signifies a filter was misserialized and is missing the
// N and/or P parameters of a serialized filter.
ErrMisserialized

// numErrorCodes is the maximum error code number used in tests.
numErrorCodes
ErrMisserialized = ErrorKind("ErrMisserialized")
)

// Map of ErrorCode values back to their constant names for pretty printing.
var errorCodeStrings = map[ErrorCode]string{
ErrNTooBig: "ErrNTooBig",
ErrPTooBig: "ErrPTooBig",
ErrBTooBig: "ErrBTooBig",
ErrMisserialized: "ErrMisserialized",
}

// String returns the ErrorCode as a human-readable name.
func (e ErrorCode) String() string {
if s := errorCodeStrings[e]; s != "" {
return s
}
return fmt.Sprintf("Unknown ErrorCode (%d)", int(e))
// Error satisfies the error interface and prints human-readable errors.
func (e ErrorKind) Error() string {
return string(e)
}

// Error identifies a filter-related error. The caller can use type assertions
// to access the ErrorCode field to ascertain the specific reason for the
// failure.
// Error identifies an error related to gcs filters. It has full support for
// errors.Is and errors.As, so the caller can ascertain the specific reason
// for the error by checking the underlying error.
type Error struct {
ErrorCode ErrorCode // Describes the kind of error
Description string // Human readable description of the issue
Err error
Description string
}

// Error satisfies the error interface and prints human-readable errors.
func (e Error) Error() string {
return e.Description
}

// makeError creates an Error given a set of arguments. The error code must
// be one of the error codes provided by this package.
func makeError(c ErrorCode, desc string) Error {
return Error{ErrorCode: c, Description: desc}
// Unwrap returns the underlying wrapped error.
func (e Error) Unwrap() error {
return e.Err
}

// IsErrorCode returns whether err is an Error with a matching error code.
func IsErrorCode(err error, c ErrorCode) bool {
e, ok := err.(Error)
return ok && e.ErrorCode == c
// makeError creates an Error given a set of arguments.
func makeError(kind ErrorKind, desc string) Error {
return Error{Err: kind, Description: desc}
}
125 changes: 76 additions & 49 deletions gcs/error_test.go
Expand Up @@ -5,33 +5,27 @@
package gcs

import (
"errors"
"io"
"testing"
)

// TestErrorCodeStringer tests the stringized output for the ErrorCode type.
func TestErrorCodeStringer(t *testing.T) {
// TestErrorKindStringer tests the stringized output for the ErrorKind type.
func TestErrorKindStringer(t *testing.T) {
tests := []struct {
in ErrorCode
in ErrorKind
want string
}{
{ErrNTooBig, "ErrNTooBig"},
{ErrPTooBig, "ErrPTooBig"},
{ErrBTooBig, "ErrBTooBig"},
{ErrMisserialized, "ErrMisserialized"},
{0xffff, "Unknown ErrorCode (65535)"},
}

// Detect additional error codes that don't have the stringer added.
if len(tests)-1 != int(numErrorCodes) {
t.Errorf("It appears an error code was added without adding an " +
"associated stringer test")
}

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.String()
result := test.in.Error()
if result != test.want {
t.Errorf("String #%d\n got: %s want: %s", i, result, test.want)
t.Errorf("%d: got: %s want: %s", i, result, test.want)
continue
}
}
Expand All @@ -51,59 +45,92 @@ func TestError(t *testing.T) {
},
}

t.Logf("Running %d tests", len(tests))
for i, test := range tests {
result := test.in.Error()
if result != test.want {
t.Errorf("Error #%d\n got: %s want: %s", i, result, test.want)
t.Errorf("%d: got: %s want: %s", i, result, test.want)
continue
}
}
}

// TestIsErrorCode ensures IsErrorCode works as intended.
func TestIsErrorCode(t *testing.T) {
// TestErrorKindIsAs ensures both ErrorKind and Error can be identified as being
// a specific error kind via errors.Is and unwrapped via errors.As.
func TestErrorKindIsAs(t *testing.T) {
tests := []struct {
name string
err error
code ErrorCode
want bool
name string
err error
target error
wantMatch bool
wantAs ErrorKind
}{{
name: "ErrNTooBig testing for ErrNTooBig",
err: makeError(ErrNTooBig, ""),
code: ErrNTooBig,
want: true,
name: "ErrNTooBig == ErrNTooBig",
err: ErrNTooBig,
target: ErrNTooBig,
wantMatch: true,
wantAs: ErrNTooBig,
}, {
name: "ErrPTooBig testing for ErrPTooBig",
err: makeError(ErrPTooBig, ""),
code: ErrPTooBig,
want: true,
name: "Error.ErrNTooBig == ErrNTooBig",
err: makeError(ErrNTooBig, ""),
target: ErrNTooBig,
wantMatch: true,
wantAs: ErrNTooBig,
}, {
name: "ErrMisserialized testing for ErrMisserialized",
err: makeError(ErrMisserialized, ""),
code: ErrMisserialized,
want: true,
name: "Error.ErrNTooBig == Error.ErrNTooBig",
err: makeError(ErrNTooBig, ""),
target: makeError(ErrNTooBig, ""),
wantMatch: true,
wantAs: ErrNTooBig,
}, {
name: "ErrNTooBig error testing for ErrPTooBig",
err: makeError(ErrNTooBig, ""),
code: ErrPTooBig,
want: false,
name: "ErrNTooBig != ErrPTooBig",
err: ErrNTooBig,
target: ErrPTooBig,
wantMatch: false,
wantAs: ErrNTooBig,
}, {
name: "ErrNTooBig error testing for unknown error code",
err: makeError(ErrNTooBig, ""),
code: 0xffff,
want: false,
name: "Error.ErrNTooBig != ErrPTooBig",
err: makeError(ErrNTooBig, ""),
target: ErrPTooBig,
wantMatch: false,
wantAs: ErrNTooBig,
}, {
name: "nil error testing for ErrNTooBig",
err: nil,
code: ErrNTooBig,
want: false,
name: "ErrNTooBig != Error.ErrPTooBig",
err: ErrNTooBig,
target: makeError(ErrPTooBig, ""),
wantMatch: false,
wantAs: ErrNTooBig,
}, {
name: "Error.ErrNTooBig != Error.ErrPTooBig",
err: makeError(ErrNTooBig, ""),
target: makeError(ErrPTooBig, ""),
wantMatch: false,
wantAs: ErrNTooBig,
}, {
name: "Error.ErrMisserialized != io.EOF",
err: makeError(ErrMisserialized, ""),
target: io.EOF,
wantMatch: false,
wantAs: ErrMisserialized,
}}
for _, test := range tests {
result := IsErrorCode(test.err, test.code)
if result != test.want {
t.Errorf("%s: unexpected result -- got: %v want: %v", test.name,
result, test.want)
// Ensure the error matches or not depending on the expected result.
result := errors.Is(test.err, test.target)
if result != test.wantMatch {
t.Errorf("%s: incorrect error identification -- got %v, want %v",
test.name, result, test.wantMatch)
continue
}

// Ensure the underlying error kind can be unwrapped and is the
// expected kind.
var kind ErrorKind
if !errors.As(test.err, &kind) {
t.Errorf("%s: unable to unwrap to error kind", test.name)
continue
}
if kind != test.wantAs {
t.Errorf("%s: unexpected unwrapped error kind -- got %v, want %v",
test.name, kind, test.wantAs)
continue
}
}
Expand Down
11 changes: 6 additions & 5 deletions gcs/gcs_test.go
Expand Up @@ -8,6 +8,7 @@ import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"math/rand"
"testing"
"time"
Expand Down Expand Up @@ -465,12 +466,12 @@ func TestFilterCorners(t *testing.T) {
const largeP = 33
var key [KeySize]byte
_, err := NewFilterV1(largeP, key, nil)
if !IsErrorCode(err, ErrPTooBig) {
if !errors.Is(err, ErrPTooBig) {
t.Fatalf("did not receive expected err for P too big -- got %v, want %v",
err, ErrPTooBig)
}
_, err = FromBytesV1(largeP, nil)
if !IsErrorCode(err, ErrPTooBig) {
if !errors.Is(err, ErrPTooBig) {
t.Fatalf("did not receive expected err for P too big -- got %v, want %v",
err, ErrPTooBig)
}
Expand All @@ -479,19 +480,19 @@ func TestFilterCorners(t *testing.T) {
const largeB = 33
const smallM = 1 << 10
_, err = NewFilterV2(largeB, smallM, key, nil)
if !IsErrorCode(err, ErrBTooBig) {
if !errors.Is(err, ErrBTooBig) {
t.Fatalf("did not receive expected err for B too big -- got %v, want %v",
err, ErrBTooBig)
}
_, err = FromBytesV2(largeB, smallM, nil)
if !IsErrorCode(err, ErrBTooBig) {
if !errors.Is(err, ErrBTooBig) {
t.Fatalf("did not receive expected err for B too big -- got %v, want %v",
err, ErrBTooBig)
}

// Attempt to decode a v1 filter without the N value serialized properly.
_, err = FromBytesV1(20, []byte{0x00})
if !IsErrorCode(err, ErrMisserialized) {
if !errors.Is(err, ErrMisserialized) {
t.Fatalf("did not receive expected err -- got %v, want %v", err,
ErrMisserialized)
}
Expand Down
2 changes: 1 addition & 1 deletion gcs/go.mod
@@ -1,6 +1,6 @@
module github.com/decred/dcrd/gcs/v3

go 1.11
go 1.13

require (
github.com/dchest/siphash v1.2.1
Expand Down

0 comments on commit e092918

Please sign in to comment.