github.com/grailbio/base@v0.0.11/errors/errors_test.go (about)

     1  // Copyright 2018 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package errors_test
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/gob"
    11  	goerrors "errors"
    12  	"fmt"
    13  	"os"
    14  	"strconv"
    15  	"testing"
    16  	"time"
    17  
    18  	fuzz "github.com/google/gofuzz"
    19  	"github.com/grailbio/base/errors"
    20  	"github.com/grailbio/base/vcontext"
    21  	"v.io/v23/verror"
    22  )
    23  
    24  // generate random errors and test encoding, etc.  (fuzz)
    25  
    26  func TestError(t *testing.T) {
    27  	_, err := os.Open("/dev/notexist")
    28  	e1 := errors.E(errors.NotExist, "opening file", err)
    29  	if got, want := e1.Error(), "opening file: resource does not exist: open /dev/notexist: no such file or directory"; got != want {
    30  		t.Errorf("got %q, want %q", got, want)
    31  	}
    32  	e2 := errors.E(err)
    33  	if got, want := e2.Error(), "resource does not exist: open /dev/notexist: no such file or directory"; got != want {
    34  		t.Errorf("got %q, want %q", got, want)
    35  	}
    36  	for _, e := range []error{e1, e2} {
    37  		if !errors.Is(errors.NotExist, e) {
    38  			t.Errorf("error %v should be NotExist", e)
    39  		}
    40  	}
    41  }
    42  
    43  func TestErrorChaining(t *testing.T) {
    44  	_, err := os.Open("/dev/notexist")
    45  	err = errors.E("failed to open file", err)
    46  	err = errors.E(errors.Retriable, "cannot proceed", err)
    47  	if got, want := err.Error(), "cannot proceed: resource does not exist (retriable):\n\tfailed to open file: open /dev/notexist: no such file or directory"; got != want {
    48  		t.Errorf("got %q, want %q", got, want)
    49  	}
    50  }
    51  
    52  type temporaryError string
    53  
    54  func (t temporaryError) Error() string   { return string(t) }
    55  func (t temporaryError) Temporary() bool { return true }
    56  
    57  func TestIsTemporary(t *testing.T) {
    58  	for _, c := range []struct {
    59  		err       error
    60  		temporary bool
    61  	}{
    62  		{errors.E(context.DeadlineExceeded), true},
    63  		{errors.E(context.Canceled), false},
    64  		{goerrors.New("no idea"), false},
    65  		{temporaryError(""), true},
    66  		{errors.E(temporaryError(""), errors.NotExist), true},
    67  		{errors.E(errors.Temporary, "failed to open socket"), true},
    68  		{errors.E("no idea"), false},
    69  		{errors.E(errors.Fatal, "fatal error"), false},
    70  		{errors.E(errors.Retriable, "this one you can retry"), true},
    71  		{errors.E(fmt.Errorf("test")), false},
    72  	} {
    73  		if got, want := errors.IsTemporary(c.err), c.temporary; got != want {
    74  			t.Errorf("error %v: got %v, want %v", c.err, got, want)
    75  		}
    76  		if c.temporary {
    77  			continue
    78  		}
    79  		if !errors.IsTemporary(errors.E(c.err, errors.Temporary)) {
    80  			t.Errorf("error %v: temporary conversion failed", c.err)
    81  		}
    82  	}
    83  }
    84  
    85  func TestGobEncoding(t *testing.T) {
    86  	_, err := os.Open("/dev/notexist")
    87  	err = errors.E("failed to open file", err)
    88  	err = errors.E(errors.Fatal, "cannot proceed", err)
    89  
    90  	var b bytes.Buffer
    91  	if err := gob.NewEncoder(&b).Encode(errors.Recover(err)); err != nil {
    92  		t.Fatal(err)
    93  	}
    94  	e2 := new(errors.Error)
    95  	if err := gob.NewDecoder(&b).Decode(e2); err != nil {
    96  		t.Fatal(err)
    97  	}
    98  	if !errors.Match(err, e2) {
    99  		t.Errorf("error %v does not match %v", err, e2)
   100  	}
   101  }
   102  
   103  func TestGobEncodingFuzz(t *testing.T) {
   104  	fz := fuzz.New().NilChance(0).Funcs(
   105  		func(e *errors.Error, c fuzz.Continue) {
   106  			c.Fuzz(&e.Kind)
   107  			c.Fuzz(&e.Severity)
   108  			c.Fuzz(&e.Message)
   109  			if c.Float32() < 0.8 {
   110  				var e2 errors.Error
   111  				c.Fuzz(&e2)
   112  				e.Err = &e2
   113  			}
   114  		},
   115  	)
   116  
   117  	const N = 1000
   118  	for i := 0; i < N; i++ {
   119  		var err errors.Error
   120  		fz.Fuzz(&err)
   121  		var b bytes.Buffer
   122  		if err := gob.NewEncoder(&b).Encode(errors.Recover(&err)); err != nil {
   123  			t.Fatal(err)
   124  		}
   125  		e2 := new(errors.Error)
   126  		if err := gob.NewDecoder(&b).Decode(e2); err != nil {
   127  			t.Fatal(err)
   128  		}
   129  		if !errors.Match(&err, e2) {
   130  			t.Errorf("error %v does not match %v", &err, e2)
   131  		}
   132  	}
   133  }
   134  
   135  func TestMessage(t *testing.T) {
   136  	for _, c := range []struct {
   137  		err     error
   138  		message string
   139  	}{
   140  		{errors.E("hello"), "hello"},
   141  		{errors.E("hello", "world"), "hello world"},
   142  	} {
   143  		if got, want := c.err.Error(), c.message; got != want {
   144  			t.Errorf("got %v, want %v", got, want)
   145  		}
   146  	}
   147  }
   148  
   149  func TestStdInterop(t *testing.T) {
   150  	tests := []struct {
   151  		name    string
   152  		makeErr func() (cleanUp func(), _ error)
   153  		kind    errors.Kind
   154  		target  error
   155  	}{
   156  		{
   157  			"not exist",
   158  			func() (cleanUp func(), _ error) {
   159  				_, err := os.Open("/dev/notexist")
   160  				return func() {}, err
   161  			},
   162  			errors.NotExist,
   163  			os.ErrNotExist,
   164  		},
   165  		{
   166  			"canceled",
   167  			func() (cleanUp func(), _ error) {
   168  				ctx, cancel := context.WithCancel(context.Background())
   169  				cancel()
   170  				<-ctx.Done()
   171  				return func() {}, ctx.Err()
   172  			},
   173  			errors.Canceled,
   174  			context.Canceled,
   175  		},
   176  		{
   177  			"timeout",
   178  			func() (cleanUp func(), _ error) {
   179  				ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-time.Minute))
   180  				<-ctx.Done()
   181  				return cancel, ctx.Err()
   182  			},
   183  			errors.Timeout,
   184  			context.DeadlineExceeded,
   185  		},
   186  		{
   187  			"timeout interface",
   188  			func() (cleanUp func(), _ error) {
   189  				return func() {}, apparentTimeoutError{}
   190  			},
   191  			errors.Timeout,
   192  			nil, // Doesn't match a stdlib error.
   193  		},
   194  	}
   195  	for _, test := range tests {
   196  		t.Run(test.name, func(t *testing.T) {
   197  			cleanUp, err := test.makeErr()
   198  			defer cleanUp()
   199  			for errIdx, err := range []error{
   200  				err,
   201  				errors.E(err),
   202  				errors.E(err, "wrapped", errors.Fatal),
   203  			} {
   204  				t.Run(strconv.Itoa(errIdx), func(t *testing.T) {
   205  					if got, want := errors.Is(test.kind, err), true; got != want {
   206  						t.Errorf("got %v, want %v", got, want)
   207  					}
   208  					if test.target != nil {
   209  						if got, want := goerrors.Is(err, test.target), true; got != want {
   210  							t.Errorf("got %v, want %v", got, want)
   211  						}
   212  					}
   213  					// err should not match wrapped target.
   214  					if got, want := goerrors.Is(err, fmt.Errorf("%w", test.target)), false; got != want {
   215  						t.Errorf("got %v, want %v", got, want)
   216  					}
   217  				})
   218  			}
   219  		})
   220  	}
   221  }
   222  
   223  func TestVerrorInterop(t *testing.T) {
   224  	err := errors.E(verror.ErrNoAccess.Errorf(vcontext.Background(), "test error"))
   225  	if got, want := errors.Recover(err).Kind, errors.NotAllowed; got != want {
   226  		t.Errorf("got %v, want %v", got, want)
   227  	}
   228  }
   229  
   230  type apparentTimeoutError struct{}
   231  
   232  func (e apparentTimeoutError) Error() string { return "timeout" }
   233  func (e apparentTimeoutError) Timeout() bool { return true }
   234  
   235  // TestEKindDeterminism ensures that errors.E's Kind detection (based on the
   236  // cause chain of the input error) is deterministic. That is, if the input
   237  // error has multiple causes (according to goerrors.Is), E chooses one
   238  // consistently. User code that handles errors based on Kind will behave
   239  // predictably.
   240  //
   241  // This is a regression test for an issue found while introducing (*Error).Is
   242  // (D65766) which makes it easier for an error chain to match multiple causes.
   243  func TestEKindDeterminism(t *testing.T) {
   244  	const N = 100
   245  	numKind := make(map[errors.Kind]int)
   246  	for i := 0; i < N; i++ {
   247  		// Construct err with a cause chain that matches Canceled due to a
   248  		// Kind and NotExist by wrapping the stdlib error.
   249  		err := errors.E(
   250  			fmt.Errorf("%w",
   251  				errors.E("canceled", errors.Canceled,
   252  					fmt.Errorf("%w", os.ErrNotExist))))
   253  		// Sanity check: err is detected as both targets.
   254  		if got, want := goerrors.Is(err, os.ErrNotExist), true; got != want {
   255  			t.Errorf("got %v, want %v", got, want)
   256  		}
   257  		if got, want := goerrors.Is(err, context.Canceled), true; got != want {
   258  			t.Errorf("got %v, want %v", got, want)
   259  		}
   260  		numKind[err.(*errors.Error).Kind]++
   261  	}
   262  	// Now, ensure the assigned Kind is Canceled, the lower number.
   263  	if got, want := len(numKind), 1; got != want {
   264  		t.Errorf("got %v, want %v", got, want)
   265  	}
   266  	if got, want := numKind[errors.Canceled], N; got != want {
   267  		t.Errorf("got %v, want %v", got, want)
   268  	}
   269  }