github.com/cockroachdb/errors@v1.11.1/errbase/format_error_internal_test.go (about) 1 // Copyright 2020 The Cockroach Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 // implied. See the License for the specific language governing 13 // permissions and limitations under the License. 14 15 package errbase 16 17 import ( 18 goErr "errors" 19 "fmt" 20 "strings" 21 "testing" 22 23 "github.com/cockroachdb/redact" 24 ) 25 26 type wrapMini struct { 27 msg string 28 cause error 29 } 30 31 func (e *wrapMini) Error() string { 32 return e.msg 33 } 34 35 func (e *wrapMini) Unwrap() error { 36 return e.cause 37 } 38 39 type wrapElideCauses struct { 40 override string 41 causes []error 42 } 43 44 func NewWrapElideCauses(override string, errors ...error) error { 45 return &wrapElideCauses{ 46 override: override, 47 causes: errors, 48 } 49 } 50 51 func (e *wrapElideCauses) Unwrap() []error { 52 return e.causes 53 } 54 55 func (e *wrapElideCauses) SafeFormatError(p Printer) (next error) { 56 p.Print(e.override) 57 // Returning nil elides errors from remaining causal chain in the 58 // implementation of `formatErrorInternal`. 59 return nil 60 } 61 62 var _ SafeFormatter = &wrapElideCauses{} 63 64 func (e *wrapElideCauses) Error() string { 65 b := strings.Builder{} 66 b.WriteString(e.override) 67 b.WriteString(": ") 68 for i, ee := range e.causes { 69 b.WriteString(ee.Error()) 70 if i < len(e.causes)-1 { 71 b.WriteByte(' ') 72 } 73 } 74 return b.String() 75 } 76 77 type wrapNoElideCauses struct { 78 prefix string 79 causes []error 80 } 81 82 func NewWrapNoElideCauses(prefix string, errors ...error) error { 83 return &wrapNoElideCauses{ 84 prefix: prefix, 85 causes: errors, 86 } 87 } 88 89 func (e *wrapNoElideCauses) Unwrap() []error { 90 return e.causes 91 } 92 93 func (e *wrapNoElideCauses) SafeFormatError(p Printer) (next error) { 94 p.Print(e.prefix) 95 return e.causes[0] 96 } 97 98 var _ SafeFormatter = &wrapNoElideCauses{} 99 100 func (e *wrapNoElideCauses) Error() string { 101 b := strings.Builder{} 102 b.WriteString(e.prefix) 103 b.WriteString(": ") 104 for i, ee := range e.causes { 105 b.WriteString(ee.Error()) 106 if i < len(e.causes)-1 { 107 b.WriteByte(' ') 108 } 109 } 110 return b.String() 111 } 112 113 // TestFormatErrorInternal attempts to highlight some idiosyncrasies of 114 // the error formatting especially when used with multi-cause error 115 // structures. Comments on specific cases below outline some gaps that 116 // still require formatting tweaks. 117 func TestFormatErrorInternal(t *testing.T) { 118 tests := []struct { 119 name string 120 err error 121 expectedSimple string 122 expectedVerbose string 123 }{ 124 { 125 name: "single wrapper", 126 err: fmt.Errorf("%w", fmt.Errorf("a%w", goErr.New("b"))), 127 expectedSimple: "ab", 128 expectedVerbose: `ab 129 (1) 130 Wraps: (2) ab 131 Wraps: (3) b 132 Error types: (1) *fmt.wrapError (2) *fmt.wrapError (3) *errors.errorString`, 133 }, 134 { 135 name: "simple multi-wrapper", 136 err: goErr.Join(goErr.New("a"), goErr.New("b")), 137 expectedSimple: "a\nb", 138 // TODO(davidh): verbose test case should have line break 139 // between `a` and `b` on second line. 140 expectedVerbose: `a 141 (1) ab 142 Wraps: (2) b 143 Wraps: (3) a 144 Error types: (1) *errors.joinError (2) *errors.errorString (3) *errors.errorString`, 145 }, 146 { 147 name: "multi-wrapper with custom formatter and partial elide", 148 err: NewWrapNoElideCauses("A", 149 NewWrapNoElideCauses("C", goErr.New("3"), goErr.New("4")), 150 NewWrapElideCauses("B", goErr.New("1"), goErr.New("2")), 151 ), 152 expectedSimple: `A: B: C: 4: 3`, // 1 and 2 omitted because they are elided. 153 expectedVerbose: `A: B: C: 4: 3 154 (1) A 155 Wraps: (2) B 156 └─ Wraps: (3) 2 157 └─ Wraps: (4) 1 158 Wraps: (5) C 159 └─ Wraps: (6) 4 160 └─ Wraps: (7) 3 161 Error types: (1) *errbase.wrapNoElideCauses (2) *errbase.wrapElideCauses (3) *errors.errorString (4) *errors.errorString (5) *errbase.wrapNoElideCauses (6) *errors.errorString (7) *errors.errorString`, 162 }, 163 { 164 name: "multi-wrapper with custom formatter and no elide", 165 // All errors in this example omit eliding their children. 166 err: NewWrapNoElideCauses("A", 167 NewWrapNoElideCauses("B", goErr.New("1"), goErr.New("2")), 168 NewWrapNoElideCauses("C", goErr.New("3"), goErr.New("4")), 169 ), 170 expectedSimple: `A: C: 4: 3: B: 2: 1`, 171 expectedVerbose: `A: C: 4: 3: B: 2: 1 172 (1) A 173 Wraps: (2) C 174 └─ Wraps: (3) 4 175 └─ Wraps: (4) 3 176 Wraps: (5) B 177 └─ Wraps: (6) 2 178 └─ Wraps: (7) 1 179 Error types: (1) *errbase.wrapNoElideCauses (2) *errbase.wrapNoElideCauses (3) *errors.errorString (4) *errors.errorString (5) *errbase.wrapNoElideCauses (6) *errors.errorString (7) *errors.errorString`, 180 }, 181 { 182 name: "simple multi-line error", 183 err: goErr.New("a\nb\nc\nd"), 184 expectedSimple: "a\nb\nc\nd", 185 // TODO(davidh): verbose test case should preserve all 3 186 // linebreaks in original error. 187 expectedVerbose: `a 188 (1) ab 189 | 190 | c 191 | d 192 Error types: (1) *errors.errorString`, 193 }, 194 { 195 name: "two-level multi-wrapper", 196 err: goErr.Join( 197 goErr.Join(goErr.New("a"), goErr.New("b")), 198 goErr.Join(goErr.New("c"), goErr.New("d")), 199 ), 200 expectedSimple: "a\nb\nc\nd", 201 // TODO(davidh): verbose output should preserve line breaks after (1) 202 // and also after (2) and (5) in `c\nd` and `a\nb`. 203 expectedVerbose: `a 204 (1) ab 205 | 206 | c 207 | d 208 Wraps: (2) cd 209 └─ Wraps: (3) d 210 └─ Wraps: (4) c 211 Wraps: (5) ab 212 └─ Wraps: (6) b 213 └─ Wraps: (7) a 214 Error types: (1) *errors.joinError (2) *errors.joinError (3) *errors.errorString (4) *errors.errorString (5) *errors.joinError (6) *errors.errorString (7) *errors.errorString`, 215 }, 216 { 217 name: "simple multi-wrapper with single-cause chains inside", 218 err: goErr.Join( 219 fmt.Errorf("a%w", goErr.New("b")), 220 fmt.Errorf("c%w", goErr.New("d")), 221 ), 222 expectedSimple: "ab\ncd", 223 expectedVerbose: `ab 224 (1) ab 225 | cd 226 Wraps: (2) cd 227 └─ Wraps: (3) d 228 Wraps: (4) ab 229 └─ Wraps: (5) b 230 Error types: (1) *errors.joinError (2) *fmt.wrapError (3) *errors.errorString (4) *fmt.wrapError (5) *errors.errorString`, 231 }, 232 { 233 name: "multi-cause wrapper with single-cause chains inside", 234 err: goErr.Join( 235 fmt.Errorf("a%w", fmt.Errorf("b%w", fmt.Errorf("c%w", goErr.New("d")))), 236 fmt.Errorf("e%w", fmt.Errorf("f%w", fmt.Errorf("g%w", goErr.New("h")))), 237 ), 238 expectedSimple: `abcd 239 efgh`, 240 expectedVerbose: `abcd 241 (1) abcd 242 | efgh 243 Wraps: (2) efgh 244 └─ Wraps: (3) fgh 245 └─ Wraps: (4) gh 246 └─ Wraps: (5) h 247 Wraps: (6) abcd 248 └─ Wraps: (7) bcd 249 └─ Wraps: (8) cd 250 └─ Wraps: (9) d 251 Error types: (1) *errors.joinError (2) *fmt.wrapError (3) *fmt.wrapError (4) *fmt.wrapError (5) *errors.errorString (6) *fmt.wrapError (7) *fmt.wrapError (8) *fmt.wrapError (9) *errors.errorString`}, 252 { 253 name: "single cause chain with multi-cause wrapper inside with single-cause chains inside", 254 err: fmt.Errorf( 255 "prefix1: %w", 256 fmt.Errorf( 257 "prefix2: %w", 258 goErr.Join( 259 fmt.Errorf("a%w", fmt.Errorf("b%w", fmt.Errorf("c%w", goErr.New("d")))), 260 fmt.Errorf("e%w", fmt.Errorf("f%w", fmt.Errorf("g%w", goErr.New("h")))), 261 ))), 262 expectedSimple: `prefix1: prefix2: abcd 263 efgh`, 264 expectedVerbose: `prefix1: prefix2: abcd 265 (1) prefix1 266 Wraps: (2) prefix2 267 Wraps: (3) abcd 268 | efgh 269 └─ Wraps: (4) efgh 270 └─ Wraps: (5) fgh 271 └─ Wraps: (6) gh 272 └─ Wraps: (7) h 273 └─ Wraps: (8) abcd 274 └─ Wraps: (9) bcd 275 └─ Wraps: (10) cd 276 └─ Wraps: (11) d 277 Error types: (1) *fmt.wrapError (2) *fmt.wrapError (3) *errors.joinError (4) *fmt.wrapError (5) *fmt.wrapError (6) *fmt.wrapError (7) *errors.errorString (8) *fmt.wrapError (9) *fmt.wrapError (10) *fmt.wrapError (11) *errors.errorString`, 278 }, 279 { 280 name: "test wrapMini elides cause error string", 281 err: &wrapMini{"whoa: d", goErr.New("d")}, 282 expectedSimple: "whoa: d", 283 expectedVerbose: `whoa: d 284 (1) whoa 285 Wraps: (2) d 286 Error types: (1) *errbase.wrapMini (2) *errors.errorString`, 287 }, 288 } 289 for _, tt := range tests { 290 t.Run(tt.name, func(t *testing.T) { 291 fe := Formattable(tt.err) 292 s := fmt.Sprintf("%s", fe) 293 if s != tt.expectedSimple { 294 t.Errorf("\nexpected: \n%s\nbut got:\n%s\n", tt.expectedSimple, s) 295 } 296 s = fmt.Sprintf("%+v", fe) 297 if s != tt.expectedVerbose { 298 t.Errorf("\nexpected: \n%s\nbut got:\n%s\n", tt.expectedVerbose, s) 299 } 300 }) 301 } 302 } 303 304 func TestPrintEntry(t *testing.T) { 305 b := func(s string) []byte { return []byte(s) } 306 307 testCases := []struct { 308 entry formatEntry 309 exp string 310 }{ 311 {formatEntry{}, ""}, 312 {formatEntry{head: b("abc")}, " abc"}, 313 {formatEntry{head: b("abc\nxyz")}, " abc\nxyz"}, 314 {formatEntry{details: b("def")}, " def"}, 315 {formatEntry{details: b("def\nxyz")}, " def\nxyz"}, 316 {formatEntry{head: b("abc"), details: b("def")}, " abcdef"}, 317 {formatEntry{head: b("abc\nxyz"), details: b("def")}, " abc\nxyzdef"}, 318 {formatEntry{head: b("abc"), details: b("def\n | xyz")}, " abcdef\n | xyz"}, 319 {formatEntry{head: b("abc\nxyz"), details: b("def\n | xyz")}, " abc\nxyzdef\n | xyz"}, 320 } 321 322 for _, tc := range testCases { 323 s := state{} 324 s.printEntry(tc.entry) 325 if s.finalBuf.String() != tc.exp { 326 t.Errorf("%s: expected %q, got %q", tc.entry, tc.exp, s.finalBuf.String()) 327 } 328 } 329 } 330 331 func TestFormatSingleLineOutput(t *testing.T) { 332 b := func(s string) []byte { return []byte(s) } 333 testCases := []struct { 334 entries []formatEntry 335 exp string 336 }{ 337 {[]formatEntry{{}}, ``}, 338 {[]formatEntry{{head: b(`a`)}}, `a`}, 339 {[]formatEntry{{head: b(`a`)}, {head: b(`b`)}, {head: b(`c`)}}, `c: b: a`}, 340 {[]formatEntry{{}, {head: b(`b`)}}, `b`}, 341 {[]formatEntry{{head: b(`a`)}, {}}, `a`}, 342 {[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, `c: a`}, 343 {[]formatEntry{{head: b(`a`), elideShort: true}, {head: b(`b`)}}, `b`}, 344 {[]formatEntry{{head: b("abc\ndef")}, {head: b("ghi\nklm")}}, "ghi\nklm: abc\ndef"}, 345 } 346 347 for _, tc := range testCases { 348 s := state{entries: tc.entries} 349 s.formatSingleLineOutput() 350 if s.finalBuf.String() != tc.exp { 351 t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String()) 352 } 353 } 354 } 355 356 func TestPrintEntryRedactable(t *testing.T) { 357 sm := string(redact.StartMarker()) 358 em := string(redact.EndMarker()) 359 esc := string(redact.EscapeMarkers(redact.StartMarker())) 360 b := func(s string) []byte { return []byte(s) } 361 q := func(s string) string { return sm + s + em } 362 363 testCases := []struct { 364 entry formatEntry 365 exp string 366 }{ 367 // If the entries are not redactable, they may contain arbitrary 368 // characters; they get enclosed in redaction markers in the final output. 369 {formatEntry{}, ""}, 370 {formatEntry{head: b("abc")}, " " + q("abc")}, 371 {formatEntry{head: b("abc\nxyz")}, " " + q("abc") + "\n" + q("xyz")}, 372 {formatEntry{details: b("def")}, " " + q("def")}, 373 {formatEntry{details: b("def\nxyz")}, " " + q("def") + "\n" + q("xyz")}, 374 {formatEntry{head: b("abc"), details: b("def")}, " " + q("abc") + q("def")}, 375 {formatEntry{head: b("abc\nxyz"), details: b("def")}, " " + q("abc") + "\n" + q("xyz") + q("def")}, 376 {formatEntry{head: b("abc"), details: b("def\n | xyz")}, " " + q("abc") + q("def") + "\n" + q(" | xyz")}, 377 {formatEntry{head: b("abc\nxyz"), details: b("def\n | xyz")}, " " + q("abc") + "\n" + q("xyz") + q("def") + "\n" + q(" | xyz")}, 378 // If there were markers in the entry, they get escaped in the output. 379 {formatEntry{head: b("abc" + em + sm), details: b("def" + em + sm)}, " " + q("abc"+esc+esc) + q("def"+esc+esc)}, 380 381 // If the entries are redactable, then whatever characters they contain 382 // are assumed safe and copied as-is to the final output. 383 {formatEntry{redactable: true}, ""}, 384 {formatEntry{redactable: true, head: b("abc")}, " abc"}, 385 {formatEntry{redactable: true, head: b("abc\nxyz")}, " abc\nxyz"}, 386 {formatEntry{redactable: true, details: b("def")}, " def"}, 387 {formatEntry{redactable: true, details: b("def\nxyz")}, " def\nxyz"}, 388 {formatEntry{redactable: true, head: b("abc"), details: b("def")}, " abcdef"}, 389 {formatEntry{redactable: true, head: b("abc\nxyz"), details: b("def")}, " abc\nxyzdef"}, 390 {formatEntry{redactable: true, head: b("abc"), details: b("def\n | xyz")}, " abcdef\n | xyz"}, 391 {formatEntry{redactable: true, head: b("abc\nxyz"), details: b("def\n | xyz")}, " abc\nxyzdef\n | xyz"}, 392 // Entry already contains some markers. 393 {formatEntry{redactable: true, head: b("a " + q("bc")), details: b("d " + q("ef"))}, " a " + q("bc") + "d " + q("ef")}, 394 } 395 396 for _, tc := range testCases { 397 s := state{redactableOutput: true} 398 s.printEntry(tc.entry) 399 if s.finalBuf.String() != tc.exp { 400 t.Errorf("%s: expected %q, got %q", tc.entry, tc.exp, s.finalBuf.String()) 401 } 402 } 403 } 404 405 func TestFormatSingleLineOutputRedactable(t *testing.T) { 406 sm := string(redact.StartMarker()) 407 em := string(redact.EndMarker()) 408 // esc := string(redact.EscapeMarkers(redact.StartMarker())) 409 b := func(s string) []byte { return []byte(s) } 410 q := func(s string) string { return sm + s + em } 411 412 testCases := []struct { 413 entries []formatEntry 414 exp string 415 }{ 416 // If the entries are not redactable, then whatever characters they contain 417 // get enclosed within redaction markers. 418 {[]formatEntry{{}}, ``}, 419 {[]formatEntry{{head: b(`a`)}}, q(`a`)}, 420 {[]formatEntry{{head: b(`a`)}, {head: b(`b`)}, {head: b(`c`)}}, q(`c`) + ": " + q(`b`) + ": " + q(`a`)}, 421 {[]formatEntry{{}, {head: b(`b`)}}, q(`b`)}, 422 {[]formatEntry{{head: b(`a`)}, {}}, q(`a`)}, 423 {[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, q(`c`) + ": " + q(`a`)}, 424 {[]formatEntry{{head: b(`a`), elideShort: true}, {head: b(`b`)}}, q(`b`)}, 425 {[]formatEntry{{head: b("abc\ndef")}, {head: b("ghi\nklm")}}, q("ghi") + "\n" + q("klm") + ": " + q("abc") + "\n" + q("def")}, 426 427 // If some entries are redactable but not others, then 428 // only those that are redactable are passed through. 429 {[]formatEntry{{redactable: true}}, ``}, 430 {[]formatEntry{{redactable: true, head: b(`a`)}}, `a`}, 431 {[]formatEntry{{redactable: true, head: b(`a`)}, {head: b(`b`)}, {redactable: true, head: b(`c`)}}, `c: ` + q(`b`) + `: a`}, 432 433 {[]formatEntry{{redactable: true}, {head: b(`b`)}}, q(`b`)}, 434 {[]formatEntry{{}, {redactable: true, head: b(`b`)}}, `b`}, 435 {[]formatEntry{{redactable: true, head: b(`a`)}, {}}, `a`}, 436 {[]formatEntry{{head: b(`a`)}, {redactable: true}}, q(`a`)}, 437 438 {[]formatEntry{{head: b(`a`)}, {}, {head: b(`c`)}}, q(`c`) + `: ` + q(`a`)}, 439 {[]formatEntry{{head: b(`a`)}, {redactable: true}, {head: b(`c`)}}, q(`c`) + `: ` + q(`a`)}, 440 {[]formatEntry{{head: b(`a`), elideShort: true, redactable: true}, {head: b(`b`)}}, q(`b`)}, 441 {[]formatEntry{{redactable: true, head: b("abc\ndef")}, {head: b("ghi\nklm")}}, q("ghi") + "\n" + q("klm") + ": abc\ndef"}, 442 {[]formatEntry{{head: b("abc\ndef")}, {redactable: true, head: b("ghi\nklm")}}, "ghi\nklm: " + q("abc") + "\n" + q("def")}, 443 // Entry already contains some markers. 444 {[]formatEntry{{redactable: true, head: b(`a` + q(" b"))}, {redactable: true, head: b(`c ` + q("d"))}}, `c ` + q(`d`) + `: a` + q(` b`)}, 445 } 446 447 for _, tc := range testCases { 448 s := state{entries: tc.entries, redactableOutput: true} 449 s.formatSingleLineOutput() 450 if s.finalBuf.String() != tc.exp { 451 t.Errorf("%s: expected %q, got %q", tc.entries, tc.exp, s.finalBuf.String()) 452 } 453 } 454 }