Skip to content

Commit

Permalink
Refactor errors and add support for Go 1.13 errors (#124)
Browse files Browse the repository at this point in the history
* all: Implement Is, As and Unwrap for errors

* Deprecate cdp.ErrorCause in favor of Is, As, Unwrap

* Use errors.Is and errors.As where applicable

* travis: Drop Go 1.12, add Go 1.15
  • Loading branch information
mafredri committed Oct 9, 2020
1 parent 86e3bb8 commit 0ea84f3
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 54 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -6,9 +6,9 @@ addons:
chrome: stable

go:
- 1.12.x
- 1.13.x
- 1.14.x
- 1.15.x
- master

before_install:
Expand Down
2 changes: 2 additions & 0 deletions error.go
Expand Up @@ -6,4 +6,6 @@ import (

// ErrorCause returns the underlying cause for this error, if possible.
// If err does not implement causer.Cause(), then err is returned.
//
// Deprecated: Use errors.Unwrap, errors.Is or errors.As instead.
func ErrorCause(err error) error { return errors.Cause(err) }
74 changes: 48 additions & 26 deletions internal/errors/errors.go
@@ -1,21 +1,23 @@
package errors

import (
"errors"
"fmt"
"strings"
)

// Causer is an interface to access to its direct cause of the error.
type Causer interface {
Cause() error
}
// Interfaces for common error unwrapping.
type causer interface{ Cause() error }
type wrapper interface{ Unwrap() error }

// Cause returns the underlying cause for this error, if possible.
// If err does not implement Causer.Cause(), then err is returned.
// If err does not implement causer.Cause(), then err is returned.
//
// Deprecated: Use errors.Unwrap, errors.Is or errors.As instead.
func Cause(err error) error {
for err != nil {
if c, ok := err.(Causer); ok {
if c, ok := err.(wrapper); ok {
err = c.Unwrap()
} else if c, ok := err.(causer); ok {
err = c.Cause()
} else {
return err
Expand All @@ -24,16 +26,6 @@ func Cause(err error) error {
return err
}

// New returns an error that formats as the given text.
func New(text string) error {
return errors.New(text)
}

// Errorf wraps New and fmt.Sprintf.
func Errorf(format string, a ...interface{}) error {
return New(fmt.Sprintf(format, a...))
}

// Wrapf wraps an error with a message. Wrapf returns nil if error is nil.
func Wrapf(err error, format string, a ...interface{}) error {
if err == nil {
Expand All @@ -51,15 +43,12 @@ type wrapped struct {
}

var _ error = (*wrapped)(nil)
var _ Causer = (*wrapped)(nil)
var _ causer = (*wrapped)(nil)
var _ wrapper = (*wrapped)(nil)

func (e *wrapped) Error() string {
return fmt.Sprintf("%s: %s", e.msg, e.err)
}

func (e *wrapped) Cause() error {
return e.err
}
func (e *wrapped) Error() string { return fmt.Sprintf("%s: %s", e.msg, e.err.Error()) }
func (e *wrapped) Cause() error { return e.err }
func (e *wrapped) Unwrap() error { return e.err }

// Merge merges multiple errors into one.
// Merge returns nil if all errors are nil.
Expand All @@ -73,14 +62,16 @@ func Merge(err ...error) error {
if len(errs) == 0 {
return nil
}
return &merged{s: err}
return &merged{s: errs}
}

type merged struct {
s []error
}

var _ error = (*merged)(nil)
var _ causer = (*merged)(nil)
var _ wrapper = (*merged)(nil)

func (e *merged) Error() string {
var m []string
Expand All @@ -89,3 +80,34 @@ func (e *merged) Error() string {
}
return strings.Join(m, ": ")
}

// Unwrap returns only the first error, there is
// no way to create a queue of errors.
func (e *merged) Unwrap() error { return e.s[0] }

// Cause returns only the first error, there is
// no way to create a queue of errors.
func (e *merged) Cause() error { return e.s[0] }

// Is runs errors.Is on all merged errors.
func (e *merged) Is(target error) bool {
if target == nil {
return nil == e.s
}
for _, err := range e.s {
if Is(err, target) {
return true
}
}
return false
}

// As runs errors.As on all merged errors.
func (e *merged) As(target interface{}) bool {
for _, err := range e.s {
if As(err, target) {
return true
}
}
return false
}
41 changes: 41 additions & 0 deletions internal/errors/errors_test.go
Expand Up @@ -50,6 +50,47 @@ func TestMergeError(t *testing.T) {
}
}

func TestMergeErrorIs(t *testing.T) {
err1 := errors.New("first")
err2 := errors.New("second")
err3 := errors.New("third")

got := Merge(err1, err2)

if !errors.Is(got, err1) {
t.Errorf("merged error is not err1, want true, got false")
}
if !errors.Is(got, err2) {
t.Errorf("merged error is not err2, want true, got false")
}
if errors.Is(got, err3) {
t.Errorf("merged error is err3, want false, got true")
}
}

type testErrorAs struct{ msg string }

func (e testErrorAs) Error() string { return e.msg }

func TestMergeErrorAs(t *testing.T) {
err1 := &wrapped{msg: "err1"}
err2 := testErrorAs{msg: "err2"}

err := Merge(err1, err2)
got1 := &wrapped{}
if !errors.As(err, &got1) {
t.Errorf("merged error as wrapped failed, want true, got false")
} else if got1.msg != "err1" {
t.Errorf("merged error as wrapped did not assign the error, want msg=err1, got msg=%s", got1.msg)
}
got2 := testErrorAs{}
if !errors.As(err, &got2) {
t.Errorf("merged error as testErrorAs failed, want true, got false")
} else if got2.Error() != "err2" {
t.Errorf("merged error as testErrorAs did not assign the error, want msg=err2, got msg=%s", got2.Error())
}
}

func TestMergeNoError(t *testing.T) {
got := Merge(nil, nil)
if got != nil {
Expand Down
64 changes: 64 additions & 0 deletions internal/errors/stdlib.go
@@ -0,0 +1,64 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package errors

import (
stderrors "errors"
"fmt"
)

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error { return stderrors.New(text) }

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error { return stderrors.Unwrap(err) }

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
// func (m MyError) Is(target error) bool { return target == os.ErrExist }
//
// then Is(MyError{}, os.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library.
func Is(err, target error) bool { return stderrors.Is(err, target) }

// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
func As(err error, target interface{}) bool { return stderrors.As(err, target) }

// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand. It is
// invalid to include more than one %w verb or to supply it with an operand
// that does not implement the error interface. The %w verb is otherwise
// a synonym for %v.
func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) }
19 changes: 12 additions & 7 deletions protocol/internal/error.go
Expand Up @@ -11,20 +11,25 @@ type OpError struct {
Err error
}

func (e OpError) Error() string {
return fmt.Sprintf("cdp.%s: %s: %s", e.Domain, e.Op, e.Err.Error())
}

// Cause implements error causer.
func (e *OpError) Cause() error {
return e.Err
}

func (e OpError) Error() string {
return fmt.Sprintf("cdp.%s: %s: %s", e.Domain, e.Op, e.Err.Error())
// Unwrap implements Wrapper.
func (e *OpError) Unwrap() error {
return e.Err
}

type causer interface {
Cause() error
}
type causer interface{ Cause() error }
type wrapper interface{ Unwrap() error }

var (
_ error = (*OpError)(nil)
_ causer = (*OpError)(nil)
_ error = (*OpError)(nil)
_ causer = (*OpError)(nil)
_ wrapper = (*OpError)(nil)
)
5 changes: 3 additions & 2 deletions rpcc/conn.go
Expand Up @@ -25,8 +25,9 @@ func (e *closeError) Error() string {
}
return e.msg
}
func (e *closeError) Closed() bool { return true }
func (e *closeError) Cause() error { return e.err }
func (e *closeError) Closed() bool { return true }
func (e *closeError) Cause() error { return e.err }
func (e *closeError) Unwrap() error { return e.err }

var (
// ErrConnClosing indicates that the operation is illegal because
Expand Down
25 changes: 10 additions & 15 deletions session/manager.go
Expand Up @@ -70,22 +70,17 @@ func (m *Manager) watch(ev *sessionEvents, created <-chan *session, done, errC c
defer ev.Close()

isClosing := func(err error) bool {
for e := err;; {
// Test if this is an rpcc.closeError.
if v, ok := e.(interface{ Closed() bool }); ok && v.Closed() {
// Cleanup, the underlying connection was closed
// before the Manager and its context does not
// inherit from rpcc.Conn.
m.cancel()
return true
}
if v, ok := e.(errors.Causer); ok {
e = v.Cause()
} else {
break
}
// Test if this is an rpcc.closeError.
var e interface{ Closed() bool }
if ok := errors.As(err, &e); ok && e.Closed() {
// Cleanup, the underlying connection was closed
// before the Manager and its context does not
// inherit from rpcc.Conn.
m.cancel()
return true
}
if cdp.ErrorCause(err) == context.Canceled {

if errors.Is(err, context.Canceled) {
// Manager was closed.
return true
}
Expand Down
4 changes: 2 additions & 2 deletions session/manager_test.go
Expand Up @@ -64,7 +64,7 @@ func TestManager_ErrorsAreSentOnErrChan(t *testing.T) {
detached.err = errors.New("detach nope")
detached.markReady()
err := <-m.Err()
if err := errors.Cause(err); err != detached.err {
if !errors.Is(err, detached.err) {
t.Errorf("got error: %v; want: %v", err, detached.err)
}
detached.next()
Expand All @@ -73,7 +73,7 @@ func TestManager_ErrorsAreSentOnErrChan(t *testing.T) {
message.err = errors.New("message nope")
message.markReady()
err = <-m.Err()
if err := errors.Cause(err); err != message.err {
if !errors.Is(err, message.err) {
t.Errorf("got error: %v; want: %v", err, message.err)
}
message.next()
Expand Down
2 changes: 1 addition & 1 deletion session/session.go
Expand Up @@ -118,7 +118,7 @@ func dial(ctx context.Context, id target.ID, tc *cdp.Client, detachTimeout time.

err := tc.Target.DetachFromTarget(ctx,
target.NewDetachFromTargetArgs().SetSessionID(s.ID))
if err := cdp.ErrorCause(err); err == context.DeadlineExceeded {
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("session: detach timed out for session %s", s.ID)
}
return errors.Wrapf(err, "session: detach failed for session %s", s.ID)
Expand Down

0 comments on commit 0ea84f3

Please sign in to comment.