github.com/haraldrudell/parl@v0.4.176/perrors/errorglue/chain-string_test.go (about) 1 /* 2 © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package errorglue 7 8 import ( 9 "errors" 10 "fmt" 11 "reflect" 12 "strings" 13 "testing" 14 15 "github.com/haraldrudell/parl/pruntime" 16 ) 17 18 // tests a chain of errors 19 func TestChainString(t *testing.T) { 20 //t.Error("logging on") 21 // errF is a fixture with a complex error graph 22 var errF = errFixture{ 23 errorMessage: "error-message", 24 errorMsg2: "associated-error-message", 25 wrapText: "Prefix: '%w'", 26 expectedMessage: "Prefix: 'error-message'", 27 key1: "key1", 28 value1: "value1", 29 // key2 is empty string 30 key2: "", 31 value2: "value2", 32 } 33 const errorsWithStackCount = 2 34 const err1FrameLength = 1 35 36 var err, err1 error 37 var messageAct, actualString string 38 var errsWithStack []error 39 var stack pruntime.Stack 40 41 // err is errorStack written by FuncName goroutine 42 // - error 1 is errorStack “Prefix…” 43 // - error 2 is from [fmt.Errorf]: [fmt.wrapError] 44 // - error 3 is related error: two errors 45 // - — the associated error is errorStack “associated…” 46 // - error 4 is errorData 47 // - error 5 is errorData 48 // - error 6 is erorrStack 49 // - error 7 is errors.errorString 50 err = errF.createError() 51 52 // err should not be nil 53 if err == nil { 54 t.Fatal("FuncName did not update err") 55 } 56 57 // err.Error() should match 58 messageAct = err.Error() 59 if messageAct != errF.expectedMessage { 60 t.Errorf("bad error message %q expected: %q", messageAct, errF.expectedMessage) 61 } 62 63 // stack error count should match 64 errsWithStack = ErrorsWithStack(err) // error instances with stack in this error chain 65 if len(errsWithStack) != errorsWithStackCount { 66 t.Fatalf("FuncName did not add %d stack traces: %d", errorsWithStackCount, len(errsWithStack)) 67 } 68 69 // LongFormat: 70 // error-message [*errorglue.errorStack]ID: 19 IsMain: false status: running 71 // github.com/haraldrudell/parl/perrors/errorglue.(*errFixture).FuncName(0x1400008e8f0) 72 // chain-string_test.go:251 73 // cre: github.com/haraldrudell/parl/perrors/errorglue.(*errFixture).Do-chain-string_test.go:215 in goroutine 18 18 74 // error-message [*errors.errorString] 75 err1 = errsWithStack[0] 76 t.Logf("LongFormat:\n%s", ChainString(err1, LongFormat)) 77 78 // first error stack depth should match 79 stack = err1.(*errorStack).StackTrace() 80 if len(stack.Frames()) != err1FrameLength { 81 t.Errorf("Stack length not %d: %d", err1FrameLength, len(stack.Frames())) 82 } 83 84 // err: ‘Prefix: 'error-message'’ 85 t.Logf("err: ‘%s’", err) 86 87 // err DumpChain: 88 // *errorglue.errorStack 89 // *fmt.wrapError 90 // *errorglue.relatedError 91 // *errorglue.errorData 92 // *errorglue.errorData 93 // *errorglue.errorStack 94 // *errors.errorString 95 t.Logf("err DumpChain: %s", DumpChain(err)) 96 97 actualString = ChainString(err, DefaultFormat) 98 99 // DefaultFormat: ‘Prefix: 'error-message'’ 100 t.Logf("DefaultFormat: ‘%s’", actualString) 101 102 // DefaultFormat should be same as Error() 103 if actualString != errF.expectedMessage { 104 t.Errorf("FAIL DefaultFormat: %q expected: %q", actualString, errF.expectedMessage) 105 } 106 107 actualString = ChainString(err, ShortFormat) 108 109 // ShortFormat: 110 // ‘Prefix: 'error-message' at errorglue.(*errFixture).FuncName()-chain-string_test.go:195 111 // 1[associated-error-message at errorglue.(*errFixture).FuncName()-chain-string_test.go:193]’ 112 t.Logf("ShortFormat: ‘%s’", actualString) 113 114 // ShortFormat should be Error() and location 115 var expected = errF.expectedMessage + " at " + errF.stack2.Frames()[0].Loc().Short() 116 if !strings.HasPrefix(actualString, expected) { 117 t.Errorf("FAIL ShortFormat:\n%q expected:\n%q", actualString, expected) 118 } 119 120 actualString = ChainString(err, LongFormat) 121 122 // error-message 123 // github.com/haraldrudell/parl/error116.(*csTypeName).FuncName 124 // /opt/sw/privates/parl/error116/chainstring_test.go:26 125 // runtime.goexit 126 // /opt/homebrew/Cellar/go/1.17.8/libexec/src/runtime/asm_arm64.s:1133 127 t.Logf("LongFormat:\n%s", actualString) 128 } 129 130 // tests [perrrors.AppendError] 131 func TestAppended(t *testing.T) { 132 //t.Error("logging on") 133 var message1, message2 = "error1", "error2" 134 var err = NewErrorStack(errors.New(message1), pruntime.NewStack(0)) 135 var err2 = NewRelatedError(err, NewErrorStack(errors.New(message2), pruntime.NewStack(0))) 136 var prefix1 = message1 + " at errorglue." 137 var contains2 = " 1[" + message2 + " at errorglue." 138 139 var stringAct string 140 _ = 1 141 142 stringAct = ChainString(err2, ShortFormat) 143 144 // stringAct: 145 // error1 at errorglue.TestAppended()-chain-string_test.go:134 146 // 1[error2 at errorglue.TestAppended()-chain-string_test.go:135] 147 t.Logf("stringAct: %s", stringAct) 148 149 // err2 shortFormat should begin with prefix1 150 if !strings.HasPrefix(stringAct, prefix1) { 151 t.Errorf("does not start with: %q: %q", prefix1, stringAct) 152 } 153 154 // err2 shortFormat should contain message2 155 if !strings.Contains(stringAct, contains2) { 156 t.Errorf("does not contain: %q: %q", contains2, stringAct) 157 } 158 } 159 160 func TestChainStringList(t *testing.T) { 161 var errNew = errors.New("new") 162 var errErrorf1 = fmt.Errorf("errorf1 %w", errNew) 163 var stack = shortStack() 164 // errorStack is a rich error 165 // - does not modify error message 166 var errStack = NewErrorStack(errErrorf1, stack) 167 var errErrorf2 = fmt.Errorf("errorf2 %w", errStack) 168 var relatedErr = errors.New("related") 169 // err is an error graph 170 var err = NewRelatedError(errErrorf2, relatedErr) 171 // expected results for all formats 172 var formatExpMap = map[CSFormat]string{ 173 DefaultFormat: err.Error(), 174 CodeLocation: err.Error() + " at " + stack.Frames()[0].Loc().Short(), 175 ShortFormat: err.Error() + " at " + stack.Frames()[0].Loc().Short() + " 1[related]", 176 LongFormat: strings.Join([]string{ 177 err.Error() + " [" + reflect.TypeOf(err).String() + "]", 178 errErrorf2.Error() + " [" + reflect.TypeOf(errErrorf2).String() + "]", 179 errStack.Error() + " [" + reflect.TypeOf(errStack).String() + "]" + "\n" + stack.String(), 180 errErrorf1.Error() + " [" + reflect.TypeOf(errErrorf1).String() + "]", 181 errNew.Error() + " [" + reflect.TypeOf(errNew).String() + "]", 182 relatedErr.Error() + " [" + reflect.TypeOf(relatedErr).String() + "]", 183 }, "\n"), 184 ShortSuffix: stack.Frames()[0].Loc().Short(), 185 LongSuffix: strings.Join([]string{ 186 err.Error() + " [" + reflect.TypeOf(err).String() + "]", 187 stack.String(), 188 relatedErr.Error() + " [" + reflect.TypeOf(relatedErr).String() + "]", 189 }, "\n"), 190 } 191 192 var formatAct, formatExp string 193 var ok bool 194 195 // err error-chain: 196 // *errorglue.relatedError *fmt.wrapError 197 // *errorglue.errorStack *fmt.wrapError *errors.errorString 198 t.Logf("err error-chain: %s", DumpChain(err)) 199 200 for _, csFormat := range csFormatList { 201 if formatExp, ok = formatExpMap[csFormat]; !ok { 202 t.Errorf("no expected value for format: %s", csFormat) 203 } 204 formatAct = ChainString(err, csFormat) 205 206 // DefaultFormat: errorf2 errorf1 new 207 // ShortFormat: errorf2 errorf1 new at testing.tRunner()-testing.go:1595 1[related] 208 // LongFormat: errorf2 errorf1 new [*errorglue.relatedError] 209 // errorf2 errorf1 new [*fmt.wrapError] 210 // errorf1 new [*errorglue.errorStack] 211 // ID: 20 status: ‘running’ 212 // testing.tRunner(0x14000082ea0, 0x102ea5950) 213 // /opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1595 214 // Parent-ID: 1 go: testing.(*T).Run 215 // /opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1648 216 // errorf1 new [*fmt.wrapError] 217 // new [*errors.errorString] 218 // related [*errors.errorString] 219 // ShortSuffix: testing.tRunner()-testing.go:1595 220 // LongSuffix: errorf2 errorf1 new [*errorglue.relatedError] 221 // ID: 20 status: ‘running’ 222 // testing.tRunner(0x14000082ea0, 0x102ea5950) 223 // /opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1595 224 // Parent-ID: 1 go: testing.(*T).Run 225 // /opt/homebrew/Cellar/go/1.21.7/libexec/src/testing/testing.go:1648 226 // related [*errors.errorString] 227 t.Logf("%s: %s", csFormat, formatAct) 228 229 // ChainString should match 230 if formatAct != formatExp { 231 t.Errorf("FAIL format: %s:\n%q exp\n%q", 232 csFormat, formatAct, formatExp, 233 ) 234 } 235 } 236 } 237 238 // shortStack retruns a short stack slice 239 func shortStack() (stack pruntime.Stack) { return pruntime.NewStack(2) } 240 241 // uses a goroutine to create an err fixture including 242 // errorStack and errorData 243 type errFixture struct { 244 // “error-message” 245 errorMessage string 246 // “associated-error-message” 247 errorMsg2 string 248 // wrapText is first associated error 249 // - “Prefix: '%w'” 250 // - used with [fmt.Errorf] 251 wrapText string 252 // “Prefix: 'error-message'” 253 expectedMessage string 254 key1 string 255 value1 string 256 key2 string 257 value2 string 258 stack0 pruntime.Stack 259 stack1 pruntime.Stack 260 stack2 pruntime.Stack 261 } 262 263 // createError returns an error fixture 264 func (n *errFixture) createError() (err error) { 265 266 // execute goroutine FuncName to end 267 var ch = make(chan struct{}) 268 go n.FuncName(ch, &err) 269 <-ch 270 271 return 272 } 273 274 // goroutine that build n.err fixture 275 func (n *errFixture) FuncName(ch chan struct{}, errp *error) { 276 defer close(ch) 277 n.stack0 = pruntime.NewStack(0) 278 n.stack1 = pruntime.NewStack(0) 279 n.stack2 = pruntime.NewStack(0) 280 // two stack traces 281 // one associated error 282 // a key-value string and a list string 283 *errp = 284 NewErrorStack( 285 fmt.Errorf(n.wrapText, 286 NewRelatedError( 287 NewErrorData( 288 NewErrorData( 289 NewErrorStack(errors.New(n.errorMessage), n.stack0), 290 n.key1, n.value1), 291 n.key2, n.value2), 292 NewErrorStack(errors.New(n.errorMsg2), n.stack1), 293 )), 294 n.stack2) 295 }