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  }