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 }