github.com/ethersphere/bee/v2@v2.2.0/pkg/log/formatter_test.go (about) 1 // Copyright 2022 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Note: the following code is derived (borrows) from: github.com/go-logr/logr 6 7 package log 8 9 import ( 10 "bytes" 11 "encoding/json" 12 "fmt" 13 "testing" 14 15 "github.com/google/go-cmp/cmp" 16 ) 17 18 // substr is handled via reflection instead of type assertions. 19 type substr string 20 21 // point implements encoding.TextMarshaller and can be used as a map key. 22 type point struct{ x, y int } 23 24 func (p point) MarshalText() ([]byte, error) { 25 return []byte(fmt.Sprintf("(%d, %d)", p.x, p.y)), nil 26 } 27 28 // pointErr implements encoding.TextMarshaler but returns an error. 29 type pointErr struct{ x, y int } 30 31 func (p pointErr) MarshalText() ([]byte, error) { 32 return nil, fmt.Errorf("uh oh: %d, %d", p.x, p.y) 33 } 34 35 // nolint:errname 36 // marshalerTest expect to result in the MarshalLog() value when logged. 37 type marshalerTest struct{ val string } 38 39 func (marshalerTest) MarshalLog() interface{} { 40 return struct{ Inner string }{"I am a log.Marshaler"} 41 } 42 func (marshalerTest) String() string { 43 return "String(): you should not see this" 44 } 45 func (marshalerTest) Error() string { 46 return "Error(): you should not see this" 47 } 48 49 // nolint:errname 50 // marshalerPanicTest expect this to result in a panic when logged. 51 type marshalerPanicTest struct{ val string } 52 53 func (marshalerPanicTest) MarshalLog() interface{} { 54 panic("marshalerPanicTest") 55 } 56 57 // nolint:errname 58 // stringerTest expect this to result in the String() value when logged. 59 type stringerTest struct{ val string } 60 61 func (stringerTest) String() string { 62 return "I am a fmt.Stringer" 63 } 64 func (stringerTest) Error() string { 65 return "Error(): you should not see this" 66 } 67 68 // stringerPanicTest expect this to result in a panic when logged. 69 type stringerPanicTest struct{ val string } 70 71 func (stringerPanicTest) String() string { 72 panic("stringerPanicTest") 73 } 74 75 // nolint:errname 76 // errorTest expect this to result in the Error() value when logged. 77 type errorTest struct{ val string } 78 79 func (errorTest) Error() string { 80 return "I am an error" 81 } 82 83 // nolint:errname 84 // errorPanicTest expect this to result in a panic when logged. 85 type errorPanicTest struct{ val string } 86 87 func (errorPanicTest) Error() string { 88 panic("errorPanicTest") 89 } 90 91 type ( 92 jsonTagsStringTest struct { 93 String1 string `json:"string1"` // renamed 94 String2 string `json:"-"` // ignored 95 String3 string `json:"-,"` // named "-" 96 String4 string `json:"string4,omitempty"` // renamed, ignore if empty 97 String5 string `json:","` // no-op 98 String6 string `json:",omitempty"` // ignore if empty 99 } 100 101 jsonTagsBoolTest struct { 102 Bool1 bool `json:"bool1"` // renamed 103 Bool2 bool `json:"-"` // ignored 104 Bool3 bool `json:"-,"` // named "-" 105 Bool4 bool `json:"bool4,omitempty"` // renamed, ignore if empty 106 Bool5 bool `json:","` // no-op 107 Bool6 bool `json:",omitempty"` // ignore if empty 108 } 109 110 jsonTagsIntTest struct { 111 Int1 int `json:"int1"` // renamed 112 Int2 int `json:"-"` // ignored 113 Int3 int `json:"-,"` // named "-" 114 Int4 int `json:"int4,omitempty"` // renamed, ignore if empty 115 Int5 int `json:","` // no-op 116 Int6 int `json:",omitempty"` // ignore if empty 117 } 118 119 jsonTagsUintTest struct { 120 Uint1 uint `json:"uint1"` // renamed 121 Uint2 uint `json:"-"` // ignored 122 Uint3 uint `json:"-,"` // named "-" 123 Uint4 uint `json:"uint4,omitempty"` // renamed, ignore if empty 124 Uint5 uint `json:","` // no-op 125 Uint6 uint `json:",omitempty"` // ignore if empty 126 } 127 128 jsonTagsFloatTest struct { 129 Float1 float64 `json:"float1"` // renamed 130 Float2 float64 `json:"-"` // ignored 131 Float3 float64 `json:"-,"` // named "-" 132 Float4 float64 `json:"float4,omitempty"` // renamed, ignore if empty 133 Float5 float64 `json:","` // no-op 134 Float6 float64 `json:",omitempty"` // ignore if empty 135 } 136 137 jsonTagsComplexTest struct { 138 Complex1 complex128 `json:"complex1"` // renamed 139 Complex2 complex128 `json:"-"` // ignored 140 Complex3 complex128 `json:"-,"` // named "-" 141 Complex4 complex128 `json:"complex4,omitempty"` // renamed, ignore if empty 142 Complex5 complex128 `json:","` // no-op 143 Complex6 complex128 `json:",omitempty"` // ignore if empty 144 } 145 146 jsonTagsPtrTest struct { 147 Ptr1 *string `json:"ptr1"` // renamed 148 Ptr2 *string `json:"-"` // ignored 149 Ptr3 *string `json:"-,"` // named "-" 150 Ptr4 *string `json:"ptr4,omitempty"` // renamed, ignore if empty 151 Ptr5 *string `json:","` // no-op 152 Ptr6 *string `json:",omitempty"` // ignore if empty 153 } 154 155 jsonTagsArrayTest struct { 156 Array1 [2]string `json:"array1"` // renamed 157 Array2 [2]string `json:"-"` // ignored 158 Array3 [2]string `json:"-,"` // named "-" 159 Array4 [2]string `json:"array4,omitempty"` // renamed, ignore if empty 160 Array5 [2]string `json:","` // no-op 161 Array6 [2]string `json:",omitempty"` // ignore if empty 162 } 163 164 jsonTagsSliceTest struct { 165 Slice1 []string `json:"slice1"` // renamed 166 Slice2 []string `json:"-"` // ignored 167 Slice3 []string `json:"-,"` // named "-" 168 Slice4 []string `json:"slice4,omitempty"` // renamed, ignore if empty 169 Slice5 []string `json:","` // no-op 170 Slice6 []string `json:",omitempty"` // ignore if empty 171 } 172 173 jsonTagsMapTest struct { 174 Map1 map[string]string `json:"map1"` // renamed 175 Map2 map[string]string `json:"-"` // ignored 176 Map3 map[string]string `json:"-,"` // named "-" 177 Map4 map[string]string `json:"map4,omitempty"` // renamed, ignore if empty 178 Map5 map[string]string `json:","` // no-op 179 Map6 map[string]string `json:",omitempty"` // ignore if empty 180 } 181 182 InnerStructTest struct{ Inner string } 183 InnerIntTest int 184 InnerMapTest map[string]string 185 InnerSliceTest []string 186 187 embedStructTest struct { 188 InnerStructTest 189 Outer string 190 } 191 192 embedNonStructTest struct { 193 InnerIntTest 194 InnerMapTest 195 InnerSliceTest 196 } 197 198 Inner1Test InnerStructTest 199 Inner2Test InnerStructTest 200 Inner3Test InnerStructTest 201 Inner4Test InnerStructTest 202 Inner5Test InnerStructTest 203 Inner6Test InnerStructTest 204 205 embedJSONTagsTest struct { 206 Outer string 207 Inner1Test `json:"inner1"` 208 Inner2Test `json:"-"` 209 Inner3Test `json:"-,"` 210 Inner4Test `json:"inner4,omitempty"` 211 Inner5Test `json:","` 212 Inner6Test `json:"inner6,omitempty"` 213 } 214 ) 215 216 func TestPretty(t *testing.T) { 217 intPtr := func(i int) *int { return &i } 218 strPtr := func(s string) *string { return &s } 219 220 testCases := []struct { 221 val interface{} 222 exp string // used in testCases where JSON can't handle it 223 }{{ 224 val: "strval", 225 }, { 226 val: "strval\nwith\t\"escapes\"", 227 }, { 228 val: substr("substrval"), 229 }, { 230 val: substr("substrval\nwith\t\"escapes\""), 231 }, { 232 val: true, 233 }, { 234 val: false, 235 }, { 236 val: 93, 237 }, { 238 val: int8(93), 239 }, { 240 val: int16(93), 241 }, { 242 val: int32(93), 243 }, { 244 val: int64(93), 245 }, { 246 val: -93, 247 }, { 248 val: int8(-93), 249 }, { 250 val: int16(-93), 251 }, { 252 val: int32(-93), 253 }, { 254 val: int64(-93), 255 }, { 256 val: uint(93), 257 }, { 258 val: uint8(93), 259 }, { 260 val: uint16(93), 261 }, { 262 val: uint32(93), 263 }, { 264 val: uint64(93), 265 }, { 266 val: uintptr(93), 267 }, { 268 val: float32(93.76), 269 }, { 270 val: 93.76, 271 }, { 272 val: complex64(93i), 273 exp: `"(0+93i)"`, 274 }, { 275 val: 93i, 276 exp: `"(0+93i)"`, 277 }, { 278 val: intPtr(93), 279 }, { 280 val: strPtr("pstrval"), 281 }, { 282 val: []int{}, 283 }, { 284 val: []int(nil), 285 exp: `[]`, 286 }, { 287 val: []int{9, 3, 7, 6}, 288 }, { 289 val: []string{"str", "with\tescape"}, 290 }, { 291 val: []substr{"substr", "with\tescape"}, 292 }, { 293 val: [4]int{9, 3, 7, 6}, 294 }, { 295 val: [2]string{"str", "with\tescape"}, 296 }, { 297 val: [2]substr{"substr", "with\tescape"}, 298 }, { 299 val: struct { 300 Int int 301 notExported string 302 String string 303 }{ 304 93, "you should not see this", "seventy-six", 305 }, 306 }, { 307 val: map[string]int{}, 308 }, { 309 val: map[string]int(nil), 310 exp: `{}`, 311 }, { 312 val: map[string]int{ 313 "nine": 3, 314 }, 315 }, { 316 val: map[string]int{ 317 "with\tescape": 76, 318 }, 319 }, { 320 val: map[substr]int{ 321 "nine": 3, 322 }, 323 }, { 324 val: map[substr]int{ 325 "with\tescape": 76, 326 }, 327 }, { 328 val: map[int]int{ 329 9: 3, 330 }, 331 }, { 332 val: map[float64]int{ 333 9.5: 3, 334 }, 335 exp: `{"9.5":3}`, 336 }, { 337 val: map[point]int{ 338 {x: 1, y: 2}: 3, 339 }, 340 }, { 341 val: map[pointErr]int{ 342 {x: 1, y: 2}: 3, 343 }, 344 exp: `{"<error-MarshalText: uh oh: 1, 2>":3}`, 345 }, { 346 val: struct { 347 X int `json:"x"` 348 Y int `json:"y"` 349 }{ 350 93, 76, 351 }, 352 }, { 353 val: struct { 354 X []int 355 Y map[int]int 356 Z struct{ P, Q int } 357 }{ 358 []int{9, 3, 7, 6}, 359 map[int]int{9: 3}, 360 struct{ P, Q int }{9, 3}, 361 }, 362 }, { 363 val: []struct{ X, Y string }{ 364 {"nine", "three"}, 365 {"seven", "six"}, 366 {"with\t", "\tescapes"}, 367 }, 368 }, { 369 val: struct { 370 A *int 371 B *int 372 C interface{} 373 D interface{} 374 }{ 375 B: intPtr(1), 376 D: interface{}(2), 377 }, 378 }, { 379 val: marshalerTest{"foobar"}, 380 exp: `{"Inner":"I am a log.Marshaler"}`, 381 }, { 382 val: &marshalerTest{"foobar"}, 383 exp: `{"Inner":"I am a log.Marshaler"}`, 384 }, { 385 val: (*marshalerTest)(nil), 386 exp: `"<panic: value method github.com/ethersphere/bee/v2/pkg/log.marshalerTest.MarshalLog called using nil *marshalerTest pointer>"`, 387 }, { 388 val: marshalerPanicTest{"foobar"}, 389 exp: `"<panic: marshalerPanicTest>"`, 390 }, { 391 val: stringerTest{"foobar"}, 392 exp: `"I am a fmt.Stringer"`, 393 }, { 394 val: &stringerTest{"foobar"}, 395 exp: `"I am a fmt.Stringer"`, 396 }, { 397 val: (*stringerTest)(nil), 398 exp: `"<panic: value method github.com/ethersphere/bee/v2/pkg/log.stringerTest.String called using nil *stringerTest pointer>"`, 399 }, { 400 val: stringerPanicTest{"foobar"}, 401 exp: `"<panic: stringerPanicTest>"`, 402 }, { 403 val: errorTest{"foobar"}, 404 exp: `"I am an error"`, 405 }, { 406 val: &errorTest{"foobar"}, 407 exp: `"I am an error"`, 408 }, { 409 val: (*errorTest)(nil), 410 exp: `"<panic: value method github.com/ethersphere/bee/v2/pkg/log.errorTest.Error called using nil *errorTest pointer>"`, 411 }, { 412 val: errorPanicTest{"foobar"}, 413 exp: `"<panic: errorPanicTest>"`, 414 }, { 415 val: jsonTagsStringTest{ 416 String1: "v1", 417 String2: "v2", 418 String3: "v3", 419 String4: "v4", 420 String5: "v5", 421 String6: "v6", 422 }, 423 }, { 424 val: jsonTagsStringTest{}, 425 }, { 426 val: jsonTagsBoolTest{ 427 Bool1: true, 428 Bool2: true, 429 Bool3: true, 430 Bool4: true, 431 Bool5: true, 432 Bool6: true, 433 }, 434 }, { 435 val: jsonTagsBoolTest{}, 436 }, { 437 val: jsonTagsIntTest{ 438 Int1: 1, 439 Int2: 2, 440 Int3: 3, 441 Int4: 4, 442 Int5: 5, 443 Int6: 6, 444 }, 445 }, { 446 val: jsonTagsIntTest{}, 447 }, { 448 val: jsonTagsUintTest{ 449 Uint1: 1, 450 Uint2: 2, 451 Uint3: 3, 452 Uint4: 4, 453 Uint5: 5, 454 Uint6: 6, 455 }, 456 }, { 457 val: jsonTagsUintTest{}, 458 }, { 459 val: jsonTagsFloatTest{ 460 Float1: 1.1, 461 Float2: 2.2, 462 Float3: 3.3, 463 Float4: 4.4, 464 Float5: 5.5, 465 Float6: 6.6, 466 }, 467 }, { 468 val: jsonTagsFloatTest{}, 469 }, { 470 val: jsonTagsComplexTest{ 471 Complex1: 1i, 472 Complex2: 2i, 473 Complex3: 3i, 474 Complex4: 4i, 475 Complex5: 5i, 476 Complex6: 6i, 477 }, 478 exp: `{"complex1":"(0+1i)","-":"(0+3i)","complex4":"(0+4i)","Complex5":"(0+5i)","Complex6":"(0+6i)"}`, 479 }, { 480 val: jsonTagsComplexTest{}, 481 exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, 482 }, { 483 val: jsonTagsPtrTest{ 484 Ptr1: strPtr("1"), 485 Ptr2: strPtr("2"), 486 Ptr3: strPtr("3"), 487 Ptr4: strPtr("4"), 488 Ptr5: strPtr("5"), 489 Ptr6: strPtr("6"), 490 }, 491 }, { 492 val: jsonTagsPtrTest{}, 493 }, { 494 val: jsonTagsArrayTest{ 495 Array1: [2]string{"v1", "v1"}, 496 Array2: [2]string{"v2", "v2"}, 497 Array3: [2]string{"v3", "v3"}, 498 Array4: [2]string{"v4", "v4"}, 499 Array5: [2]string{"v5", "v5"}, 500 Array6: [2]string{"v6", "v6"}, 501 }, 502 }, { 503 val: jsonTagsArrayTest{}, 504 }, { 505 val: jsonTagsSliceTest{ 506 Slice1: []string{"v1", "v1"}, 507 Slice2: []string{"v2", "v2"}, 508 Slice3: []string{"v3", "v3"}, 509 Slice4: []string{"v4", "v4"}, 510 Slice5: []string{"v5", "v5"}, 511 Slice6: []string{"v6", "v6"}, 512 }, 513 }, { 514 val: jsonTagsSliceTest{}, 515 exp: `{"slice1":[],"-":[],"Slice5":[]}`, 516 }, { 517 val: jsonTagsMapTest{ 518 Map1: map[string]string{"k1": "v1"}, 519 Map2: map[string]string{"k2": "v2"}, 520 Map3: map[string]string{"k3": "v3"}, 521 Map4: map[string]string{"k4": "v4"}, 522 Map5: map[string]string{"k5": "v5"}, 523 Map6: map[string]string{"k6": "v6"}, 524 }, 525 }, { 526 val: jsonTagsMapTest{}, 527 exp: `{"map1":{},"-":{},"Map5":{}}`, 528 }, { 529 val: embedStructTest{}, 530 }, { 531 val: embedNonStructTest{}, 532 exp: `{"InnerIntTest":0,"InnerMapTest":{},"InnerSliceTest":[]}`, 533 }, { 534 val: embedJSONTagsTest{}, 535 }, { 536 val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), 537 exp: `{"f1":1,"f2":true,"f3":[]}`, 538 }, { 539 val: map[jsonTagsStringTest]int{ 540 {String1: `"quoted"`, String4: `unquoted`}: 1, 541 }, 542 exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, 543 }, { 544 val: map[jsonTagsIntTest]int{ 545 {Int1: 1, Int2: 2}: 3, 546 }, 547 exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, 548 }, { 549 val: map[[2]struct{ S string }]int{ 550 {{S: `"quoted"`}, {S: "unquoted"}}: 1, 551 }, 552 exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, 553 }, { 554 val: jsonTagsComplexTest{}, 555 exp: `{"complex1":"(0+0i)","-":"(0+0i)","Complex5":"(0+0i)"}`, 556 }, { 557 val: jsonTagsPtrTest{ 558 Ptr1: strPtr("1"), 559 Ptr2: strPtr("2"), 560 Ptr3: strPtr("3"), 561 Ptr4: strPtr("4"), 562 Ptr5: strPtr("5"), 563 Ptr6: strPtr("6"), 564 }, 565 }, { 566 val: jsonTagsPtrTest{}, 567 }, { 568 val: jsonTagsArrayTest{ 569 Array1: [2]string{"v1", "v1"}, 570 Array2: [2]string{"v2", "v2"}, 571 Array3: [2]string{"v3", "v3"}, 572 Array4: [2]string{"v4", "v4"}, 573 Array5: [2]string{"v5", "v5"}, 574 Array6: [2]string{"v6", "v6"}, 575 }, 576 }, { 577 val: jsonTagsArrayTest{}, 578 }, { 579 val: jsonTagsSliceTest{ 580 Slice1: []string{"v1", "v1"}, 581 Slice2: []string{"v2", "v2"}, 582 Slice3: []string{"v3", "v3"}, 583 Slice4: []string{"v4", "v4"}, 584 Slice5: []string{"v5", "v5"}, 585 Slice6: []string{"v6", "v6"}, 586 }, 587 }, { 588 val: jsonTagsSliceTest{}, 589 exp: `{"slice1":[],"-":[],"Slice5":[]}`, 590 }, { 591 val: jsonTagsMapTest{ 592 Map1: map[string]string{"k1": "v1"}, 593 Map2: map[string]string{"k2": "v2"}, 594 Map3: map[string]string{"k3": "v3"}, 595 Map4: map[string]string{"k4": "v4"}, 596 Map5: map[string]string{"k5": "v5"}, 597 Map6: map[string]string{"k6": "v6"}, 598 }, 599 }, { 600 val: jsonTagsMapTest{}, 601 exp: `{"map1":{},"-":{},"Map5":{}}`, 602 }, { 603 val: embedStructTest{}, 604 }, { 605 val: embedNonStructTest{}, 606 exp: `{"InnerIntTest":0,"InnerMapTest":{},"InnerSliceTest":[]}`, 607 }, { 608 val: embedJSONTagsTest{}, 609 }, { 610 val: PseudoStruct(makeKV("f1", 1, "f2", true, "f3", []int{})), 611 exp: `{"f1":1,"f2":true,"f3":[]}`, 612 }, { 613 val: map[jsonTagsStringTest]int{ 614 {String1: `"quoted"`, String4: `unquoted`}: 1, 615 }, 616 exp: `{"{\"string1\":\"\\\"quoted\\\"\",\"-\":\"\",\"string4\":\"unquoted\",\"String5\":\"\"}":1}`, 617 }, { 618 val: map[jsonTagsIntTest]int{ 619 {Int1: 1, Int2: 2}: 3, 620 }, 621 exp: `{"{\"int1\":1,\"-\":0,\"Int5\":0}":3}`, 622 }, { 623 val: map[[2]struct{ S string }]int{ 624 {{S: `"quoted"`}, {S: "unquoted"}}: 1, 625 }, 626 exp: `{"[{\"S\":\"\\\"quoted\\\"\"},{\"S\":\"unquoted\"}]":1}`, 627 }} 628 629 o := *defaults.options 630 f := newFormatter(o.fmtOptions) 631 for _, tc := range testCases { 632 t.Run("", func(t *testing.T) { 633 var want string 634 have := f.prettyWithFlags(tc.val, 0, 0) 635 636 if tc.exp != "" { 637 want = tc.exp 638 } else { 639 jb, err := json.Marshal(tc.val) 640 if err != nil { 641 t.Fatalf("unexpected error: %v\nhave: %q", err, have) 642 } 643 want = string(jb) 644 } 645 646 if have != want { 647 t.Errorf("prettyWithFlags(...):\n\twant %q\n\thave %q", want, have) 648 } 649 }) 650 } 651 } 652 653 func makeKV(args ...interface{}) []interface{} { return args } 654 655 func TestRender(t *testing.T) { 656 testCases := []struct { 657 name string 658 builtins []interface{} 659 args []interface{} 660 wantKV string 661 wantJSON string 662 }{{ 663 name: "nil", 664 wantKV: "", 665 wantJSON: "{}", 666 }, { 667 name: "empty", 668 builtins: []interface{}{}, 669 args: []interface{}{}, 670 wantKV: "", 671 wantJSON: "{}", 672 }, { 673 name: "primitives", 674 builtins: makeKV("int1", 1, "int2", 2), 675 args: makeKV("bool1", true, "bool2", false), 676 wantKV: `"int1"=1 "int2"=2 "bool1"=true "bool2"=false`, 677 wantJSON: `{"int1":1,"int2":2,"bool1":true,"bool2":false}`, 678 }, { 679 name: "pseudo structs", 680 builtins: makeKV("int", PseudoStruct(makeKV("intsub", 1))), 681 args: makeKV("bool", PseudoStruct(makeKV("boolsub", true))), 682 wantKV: `"int"={"intsub":1} "bool"={"boolsub":true}`, 683 wantJSON: `{"int":{"intsub":1},"bool":{"boolsub":true}}`, 684 }, { 685 name: "escapes", 686 builtins: makeKV("\"1\"", 1), // will not be escaped, but should never happen 687 args: makeKV("bool\n", true), // escaped 688 wantKV: `""1""=1 "bool\n"=true`, 689 wantJSON: `{""1"":1,"bool\n":true}`, 690 }, { 691 name: "missing value", 692 builtins: makeKV("builtin"), 693 args: makeKV("arg"), 694 wantKV: `"builtin"="<no-value>" "arg"="<no-value>"`, 695 wantJSON: `{"builtin":"<no-value>","arg":"<no-value>"}`, 696 }, { 697 name: "non-string key int", 698 builtins: makeKV(123, "val"), // should never happen 699 args: makeKV(456, "val"), 700 wantKV: `"<non-string-key: 123>"="val" "<non-string-key: 456>"="val"`, 701 wantJSON: `{"<non-string-key: 123>":"val","<non-string-key: 456>":"val"}`, 702 }, { 703 name: "non-string key struct", 704 builtins: makeKV(struct { // will not be escaped, but should never happen 705 F1 string 706 F2 int 707 }{"builtin", 123}, "val"), 708 args: makeKV(struct { 709 F1 string 710 F2 int 711 }{"arg", 456}, "val"), 712 wantKV: `"<non-string-key: {"F1":"builtin",>"="val" "<non-string-key: {\"F1\":\"arg\",\"F2\">"="val"`, 713 wantJSON: `{"<non-string-key: {"F1":"builtin",>":"val","<non-string-key: {\"F1\":\"arg\",\"F2\">":"val"}`, 714 }} 715 716 for _, tc := range testCases { 717 t.Run(tc.name, func(t *testing.T) { 718 test := func(t *testing.T, formatter *formatter, want string) { 719 t.Helper() 720 721 have := string(bytes.TrimRight(formatter.render(tc.builtins, tc.args), "\n")) 722 if have != want { 723 t.Errorf("render(...):\nwant %q\nhave %q", want, have) 724 } 725 } 726 t.Run("KV", func(t *testing.T) { 727 o := *defaults.options 728 test(t, newFormatter(o.fmtOptions), tc.wantKV) 729 }) 730 t.Run("JSON", func(t *testing.T) { 731 o := *defaults.options 732 WithJSONOutput()(&o) 733 test(t, newFormatter(o.fmtOptions), tc.wantJSON) 734 }) 735 }) 736 } 737 } 738 739 func TestSanitize(t *testing.T) { 740 testCases := []struct { 741 name string 742 kv []interface{} 743 want []interface{} 744 }{{ 745 name: "empty", 746 kv: []interface{}{}, 747 want: []interface{}{}, 748 }, { 749 name: "already sane", 750 kv: makeKV("int", 1, "str", "ABC", "bool", true), 751 want: makeKV("int", 1, "str", "ABC", "bool", true), 752 }, { 753 name: "missing value", 754 kv: makeKV("key"), 755 want: makeKV("key", "<no-value>"), 756 }, { 757 name: "non-string key int", 758 kv: makeKV(123, "val"), 759 want: makeKV("<non-string-key: 123>", "val"), 760 }, { 761 name: "non-string key struct", 762 kv: makeKV(struct { 763 F1 string 764 F2 int 765 }{"f1", 8675309}, "val"), 766 want: makeKV(`<non-string-key: {"F1":"f1","F2":>`, "val"), 767 }} 768 769 o := *defaults.options 770 WithJSONOutput()(&o) 771 f := newFormatter(o.fmtOptions) 772 for _, tc := range testCases { 773 t.Run(tc.name, func(t *testing.T) { 774 have := f.sanitize(tc.kv) 775 if diff := cmp.Diff(have, tc.want); diff != "" { 776 t.Errorf("sanitize(...) mismatch (-want +have):\n%s", diff) 777 } 778 }) 779 } 780 }