Source file src/log/slog/handler_test.go

     1  // Copyright 2022 The Go 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  // TODO: verify that the output of Marshal{Text,JSON} is suitably escaped.
     6  
     7  package slog
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"encoding/json"
    13  	"io"
    14  	"path/filepath"
    15  	"slices"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"testing"
    20  	"time"
    21  )
    22  
    23  func TestDefaultHandle(t *testing.T) {
    24  	ctx := context.Background()
    25  	preAttrs := []Attr{Int("pre", 0)}
    26  	attrs := []Attr{Int("a", 1), String("b", "two")}
    27  	for _, test := range []struct {
    28  		name  string
    29  		with  func(Handler) Handler
    30  		attrs []Attr
    31  		want  string
    32  	}{
    33  		{
    34  			name: "no attrs",
    35  			want: "INFO message",
    36  		},
    37  		{
    38  			name:  "attrs",
    39  			attrs: attrs,
    40  			want:  "INFO message a=1 b=two",
    41  		},
    42  		{
    43  			name:  "preformatted",
    44  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs) },
    45  			attrs: attrs,
    46  			want:  "INFO message pre=0 a=1 b=two",
    47  		},
    48  		{
    49  			name: "groups",
    50  			attrs: []Attr{
    51  				Int("a", 1),
    52  				Group("g",
    53  					Int("b", 2),
    54  					Group("h", Int("c", 3)),
    55  					Int("d", 4)),
    56  				Int("e", 5),
    57  			},
    58  			want: "INFO message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
    59  		},
    60  		{
    61  			name:  "group",
    62  			with:  func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
    63  			attrs: attrs,
    64  			want:  "INFO message pre=0 s.a=1 s.b=two",
    65  		},
    66  		{
    67  			name: "preformatted groups",
    68  			with: func(h Handler) Handler {
    69  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    70  					WithGroup("s1").
    71  					WithAttrs([]Attr{Int("p2", 2)}).
    72  					WithGroup("s2")
    73  			},
    74  			attrs: attrs,
    75  			want:  "INFO message p1=1 s1.p2=2 s1.s2.a=1 s1.s2.b=two",
    76  		},
    77  		{
    78  			name: "two with-groups",
    79  			with: func(h Handler) Handler {
    80  				return h.WithAttrs([]Attr{Int("p1", 1)}).
    81  					WithGroup("s1").
    82  					WithGroup("s2")
    83  			},
    84  			attrs: attrs,
    85  			want:  "INFO message p1=1 s1.s2.a=1 s1.s2.b=two",
    86  		},
    87  	} {
    88  		t.Run(test.name, func(t *testing.T) {
    89  			var got string
    90  			var h Handler = newDefaultHandler(func(_ uintptr, b []byte) error {
    91  				got = string(b)
    92  				return nil
    93  			})
    94  			if test.with != nil {
    95  				h = test.with(h)
    96  			}
    97  			r := NewRecord(time.Time{}, LevelInfo, "message", 0)
    98  			r.AddAttrs(test.attrs...)
    99  			if err := h.Handle(ctx, r); err != nil {
   100  				t.Fatal(err)
   101  			}
   102  			if got != test.want {
   103  				t.Errorf("\ngot  %s\nwant %s", got, test.want)
   104  			}
   105  		})
   106  	}
   107  }
   108  
   109  func TestConcurrentWrites(t *testing.T) {
   110  	ctx := context.Background()
   111  	count := 1000
   112  	for _, handlerType := range []string{"text", "json"} {
   113  		t.Run(handlerType, func(t *testing.T) {
   114  			var buf bytes.Buffer
   115  			var h Handler
   116  			switch handlerType {
   117  			case "text":
   118  				h = NewTextHandler(&buf, nil)
   119  			case "json":
   120  				h = NewJSONHandler(&buf, nil)
   121  			default:
   122  				t.Fatalf("unexpected handlerType %q", handlerType)
   123  			}
   124  			sub1 := h.WithAttrs([]Attr{Bool("sub1", true)})
   125  			sub2 := h.WithAttrs([]Attr{Bool("sub2", true)})
   126  			var wg sync.WaitGroup
   127  			for i := 0; i < count; i++ {
   128  				sub1Record := NewRecord(time.Time{}, LevelInfo, "hello from sub1", 0)
   129  				sub1Record.AddAttrs(Int("i", i))
   130  				sub2Record := NewRecord(time.Time{}, LevelInfo, "hello from sub2", 0)
   131  				sub2Record.AddAttrs(Int("i", i))
   132  				wg.Add(1)
   133  				go func() {
   134  					defer wg.Done()
   135  					if err := sub1.Handle(ctx, sub1Record); err != nil {
   136  						t.Error(err)
   137  					}
   138  					if err := sub2.Handle(ctx, sub2Record); err != nil {
   139  						t.Error(err)
   140  					}
   141  				}()
   142  			}
   143  			wg.Wait()
   144  			for i := 1; i <= 2; i++ {
   145  				want := "hello from sub" + strconv.Itoa(i)
   146  				n := strings.Count(buf.String(), want)
   147  				if n != count {
   148  					t.Fatalf("want %d occurrences of %q, got %d", count, want, n)
   149  				}
   150  			}
   151  		})
   152  	}
   153  }
   154  
   155  // Verify the common parts of TextHandler and JSONHandler.
   156  func TestJSONAndTextHandlers(t *testing.T) {
   157  	// remove all Attrs
   158  	removeAll := func(_ []string, a Attr) Attr { return Attr{} }
   159  
   160  	attrs := []Attr{String("a", "one"), Int("b", 2), Any("", nil)}
   161  	preAttrs := []Attr{Int("pre", 3), String("x", "y")}
   162  
   163  	for _, test := range []struct {
   164  		name      string
   165  		replace   func([]string, Attr) Attr
   166  		addSource bool
   167  		with      func(Handler) Handler
   168  		preAttrs  []Attr
   169  		attrs     []Attr
   170  		wantText  string
   171  		wantJSON  string
   172  	}{
   173  		{
   174  			name:     "basic",
   175  			attrs:    attrs,
   176  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2",
   177  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2}`,
   178  		},
   179  		{
   180  			name:     "empty key",
   181  			attrs:    append(slices.Clip(attrs), Any("", "v")),
   182  			wantText: `time=2000-01-02T03:04:05.000Z level=INFO msg=message a=one b=2 ""=v`,
   183  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","a":"one","b":2,"":"v"}`,
   184  		},
   185  		{
   186  			name:     "cap keys",
   187  			replace:  upperCaseKey,
   188  			attrs:    attrs,
   189  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message A=one B=2",
   190  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","A":"one","B":2}`,
   191  		},
   192  		{
   193  			name:     "remove all",
   194  			replace:  removeAll,
   195  			attrs:    attrs,
   196  			wantText: "",
   197  			wantJSON: `{}`,
   198  		},
   199  		{
   200  			name:     "preformatted",
   201  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   202  			preAttrs: preAttrs,
   203  			attrs:    attrs,
   204  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message pre=3 x=y a=one b=2",
   205  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","pre":3,"x":"y","a":"one","b":2}`,
   206  		},
   207  		{
   208  			name:     "preformatted cap keys",
   209  			replace:  upperCaseKey,
   210  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   211  			preAttrs: preAttrs,
   212  			attrs:    attrs,
   213  			wantText: "TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG=message PRE=3 X=y A=one B=2",
   214  			wantJSON: `{"TIME":"2000-01-02T03:04:05Z","LEVEL":"INFO","MSG":"message","PRE":3,"X":"y","A":"one","B":2}`,
   215  		},
   216  		{
   217  			name:     "preformatted remove all",
   218  			replace:  removeAll,
   219  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   220  			preAttrs: preAttrs,
   221  			attrs:    attrs,
   222  			wantText: "",
   223  			wantJSON: "{}",
   224  		},
   225  		{
   226  			name:     "remove built-in",
   227  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   228  			attrs:    attrs,
   229  			wantText: "a=one b=2",
   230  			wantJSON: `{"a":"one","b":2}`,
   231  		},
   232  		{
   233  			name:     "preformatted remove built-in",
   234  			replace:  removeKeys(TimeKey, LevelKey, MessageKey),
   235  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs) },
   236  			attrs:    attrs,
   237  			wantText: "pre=3 x=y a=one b=2",
   238  			wantJSON: `{"pre":3,"x":"y","a":"one","b":2}`,
   239  		},
   240  		{
   241  			name:    "groups",
   242  			replace: removeKeys(TimeKey, LevelKey), // to simplify the result
   243  			attrs: []Attr{
   244  				Int("a", 1),
   245  				Group("g",
   246  					Int("b", 2),
   247  					Group("h", Int("c", 3)),
   248  					Int("d", 4)),
   249  				Int("e", 5),
   250  			},
   251  			wantText: "msg=message a=1 g.b=2 g.h.c=3 g.d=4 e=5",
   252  			wantJSON: `{"msg":"message","a":1,"g":{"b":2,"h":{"c":3},"d":4},"e":5}`,
   253  		},
   254  		{
   255  			name:     "empty group",
   256  			replace:  removeKeys(TimeKey, LevelKey),
   257  			attrs:    []Attr{Group("g"), Group("h", Int("a", 1))},
   258  			wantText: "msg=message h.a=1",
   259  			wantJSON: `{"msg":"message","h":{"a":1}}`,
   260  		},
   261  		{
   262  			name:    "nested empty group",
   263  			replace: removeKeys(TimeKey, LevelKey),
   264  			attrs: []Attr{
   265  				Group("g",
   266  					Group("h",
   267  						Group("i"), Group("j"))),
   268  			},
   269  			wantText: `msg=message`,
   270  			wantJSON: `{"msg":"message"}`,
   271  		},
   272  		{
   273  			name:    "nested non-empty group",
   274  			replace: removeKeys(TimeKey, LevelKey),
   275  			attrs: []Attr{
   276  				Group("g",
   277  					Group("h",
   278  						Group("i"), Group("j", Int("a", 1)))),
   279  			},
   280  			wantText: `msg=message g.h.j.a=1`,
   281  			wantJSON: `{"msg":"message","g":{"h":{"j":{"a":1}}}}`,
   282  		},
   283  		{
   284  			name:    "escapes",
   285  			replace: removeKeys(TimeKey, LevelKey),
   286  			attrs: []Attr{
   287  				String("a b", "x\t\n\000y"),
   288  				Group(" b.c=\"\\x2E\t",
   289  					String("d=e", "f.g\""),
   290  					Int("m.d", 1)), // dot is not escaped
   291  			},
   292  			wantText: `msg=message "a b"="x\t\n\x00y" " b.c=\"\\x2E\t.d=e"="f.g\"" " b.c=\"\\x2E\t.m.d"=1`,
   293  			wantJSON: `{"msg":"message","a b":"x\t\n\u0000y"," b.c=\"\\x2E\t":{"d=e":"f.g\"","m.d":1}}`,
   294  		},
   295  		{
   296  			name:    "LogValuer",
   297  			replace: removeKeys(TimeKey, LevelKey),
   298  			attrs: []Attr{
   299  				Int("a", 1),
   300  				Any("name", logValueName{"Ren", "Hoek"}),
   301  				Int("b", 2),
   302  			},
   303  			wantText: "msg=message a=1 name.first=Ren name.last=Hoek b=2",
   304  			wantJSON: `{"msg":"message","a":1,"name":{"first":"Ren","last":"Hoek"},"b":2}`,
   305  		},
   306  		{
   307  			// Test resolution when there is no ReplaceAttr function.
   308  			name: "resolve",
   309  			attrs: []Attr{
   310  				Any("", &replace{Value{}}), // should be elided
   311  				Any("name", logValueName{"Ren", "Hoek"}),
   312  			},
   313  			wantText: "time=2000-01-02T03:04:05.000Z level=INFO msg=message name.first=Ren name.last=Hoek",
   314  			wantJSON: `{"time":"2000-01-02T03:04:05Z","level":"INFO","msg":"message","name":{"first":"Ren","last":"Hoek"}}`,
   315  		},
   316  		{
   317  			name:     "with-group",
   318  			replace:  removeKeys(TimeKey, LevelKey),
   319  			with:     func(h Handler) Handler { return h.WithAttrs(preAttrs).WithGroup("s") },
   320  			attrs:    attrs,
   321  			wantText: "msg=message pre=3 x=y s.a=one s.b=2",
   322  			wantJSON: `{"msg":"message","pre":3,"x":"y","s":{"a":"one","b":2}}`,
   323  		},
   324  		{
   325  			name:    "preformatted with-groups",
   326  			replace: removeKeys(TimeKey, LevelKey),
   327  			with: func(h Handler) Handler {
   328  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   329  					WithGroup("s1").
   330  					WithAttrs([]Attr{Int("p2", 2)}).
   331  					WithGroup("s2").
   332  					WithAttrs([]Attr{Int("p3", 3)})
   333  			},
   334  			attrs:    attrs,
   335  			wantText: "msg=message p1=1 s1.p2=2 s1.s2.p3=3 s1.s2.a=one s1.s2.b=2",
   336  			wantJSON: `{"msg":"message","p1":1,"s1":{"p2":2,"s2":{"p3":3,"a":"one","b":2}}}`,
   337  		},
   338  		{
   339  			name:    "two with-groups",
   340  			replace: removeKeys(TimeKey, LevelKey),
   341  			with: func(h Handler) Handler {
   342  				return h.WithAttrs([]Attr{Int("p1", 1)}).
   343  					WithGroup("s1").
   344  					WithGroup("s2")
   345  			},
   346  			attrs:    attrs,
   347  			wantText: "msg=message p1=1 s1.s2.a=one s1.s2.b=2",
   348  			wantJSON: `{"msg":"message","p1":1,"s1":{"s2":{"a":"one","b":2}}}`,
   349  		},
   350  		{
   351  			name:    "empty with-groups",
   352  			replace: removeKeys(TimeKey, LevelKey),
   353  			with: func(h Handler) Handler {
   354  				return h.WithGroup("x").WithGroup("y")
   355  			},
   356  			wantText: "msg=message",
   357  			wantJSON: `{"msg":"message"}`,
   358  		},
   359  		{
   360  			name:    "empty with-groups, no non-empty attrs",
   361  			replace: removeKeys(TimeKey, LevelKey),
   362  			with: func(h Handler) Handler {
   363  				return h.WithGroup("x").WithAttrs([]Attr{Group("g")}).WithGroup("y")
   364  			},
   365  			wantText: "msg=message",
   366  			wantJSON: `{"msg":"message"}`,
   367  		},
   368  		{
   369  			name:    "one empty with-group",
   370  			replace: removeKeys(TimeKey, LevelKey),
   371  			with: func(h Handler) Handler {
   372  				return h.WithGroup("x").WithAttrs([]Attr{Int("a", 1)}).WithGroup("y")
   373  			},
   374  			attrs:    []Attr{Group("g", Group("h"))},
   375  			wantText: "msg=message x.a=1",
   376  			wantJSON: `{"msg":"message","x":{"a":1}}`,
   377  		},
   378  		{
   379  			name:     "GroupValue as Attr value",
   380  			replace:  removeKeys(TimeKey, LevelKey),
   381  			attrs:    []Attr{{"v", AnyValue(IntValue(3))}},
   382  			wantText: "msg=message v=3",
   383  			wantJSON: `{"msg":"message","v":3}`,
   384  		},
   385  		{
   386  			name:     "byte slice",
   387  			replace:  removeKeys(TimeKey, LevelKey),
   388  			attrs:    []Attr{Any("bs", []byte{1, 2, 3, 4})},
   389  			wantText: `msg=message bs="\x01\x02\x03\x04"`,
   390  			wantJSON: `{"msg":"message","bs":"AQIDBA=="}`,
   391  		},
   392  		{
   393  			name:     "json.RawMessage",
   394  			replace:  removeKeys(TimeKey, LevelKey),
   395  			attrs:    []Attr{Any("bs", json.RawMessage([]byte("1234")))},
   396  			wantText: `msg=message bs="1234"`,
   397  			wantJSON: `{"msg":"message","bs":1234}`,
   398  		},
   399  		{
   400  			name:    "inline group",
   401  			replace: removeKeys(TimeKey, LevelKey),
   402  			attrs: []Attr{
   403  				Int("a", 1),
   404  				Group("", Int("b", 2), Int("c", 3)),
   405  				Int("d", 4),
   406  			},
   407  			wantText: `msg=message a=1 b=2 c=3 d=4`,
   408  			wantJSON: `{"msg":"message","a":1,"b":2,"c":3,"d":4}`,
   409  		},
   410  		{
   411  			name: "Source",
   412  			replace: func(gs []string, a Attr) Attr {
   413  				if a.Key == SourceKey {
   414  					s := a.Value.Any().(*Source)
   415  					s.File = filepath.Base(s.File)
   416  					return Any(a.Key, s)
   417  				}
   418  				return removeKeys(TimeKey, LevelKey)(gs, a)
   419  			},
   420  			addSource: true,
   421  			wantText:  `source=handler_test.go:$LINE msg=message`,
   422  			wantJSON:  `{"source":{"function":"log/slog.TestJSONAndTextHandlers","file":"handler_test.go","line":$LINE},"msg":"message"}`,
   423  		},
   424  		{
   425  			name: "replace built-in with group",
   426  			replace: func(_ []string, a Attr) Attr {
   427  				if a.Key == TimeKey {
   428  					return Group(TimeKey, "mins", 3, "secs", 2)
   429  				}
   430  				if a.Key == LevelKey {
   431  					return Attr{}
   432  				}
   433  				return a
   434  			},
   435  			wantText: `time.mins=3 time.secs=2 msg=message`,
   436  			wantJSON: `{"time":{"mins":3,"secs":2},"msg":"message"}`,
   437  		},
   438  		{
   439  			name:     "replace empty",
   440  			replace:  func([]string, Attr) Attr { return Attr{} },
   441  			attrs:    []Attr{Group("g", Int("a", 1))},
   442  			wantText: "",
   443  			wantJSON: `{}`,
   444  		},
   445  		{
   446  			name: "replace empty 1",
   447  			with: func(h Handler) Handler {
   448  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)})
   449  			},
   450  			replace:  func([]string, Attr) Attr { return Attr{} },
   451  			attrs:    []Attr{Group("h", Int("b", 2))},
   452  			wantText: "",
   453  			wantJSON: `{}`,
   454  		},
   455  		{
   456  			name: "replace empty 2",
   457  			with: func(h Handler) Handler {
   458  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   459  			},
   460  			replace:  func([]string, Attr) Attr { return Attr{} },
   461  			attrs:    []Attr{Group("i", Int("c", 3))},
   462  			wantText: "",
   463  			wantJSON: `{}`,
   464  		},
   465  		{
   466  			name:     "replace empty 3",
   467  			with:     func(h Handler) Handler { return h.WithGroup("g") },
   468  			replace:  func([]string, Attr) Attr { return Attr{} },
   469  			attrs:    []Attr{Int("a", 1)},
   470  			wantText: "",
   471  			wantJSON: `{}`,
   472  		},
   473  		{
   474  			name: "replace empty inline",
   475  			with: func(h Handler) Handler {
   476  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   477  			},
   478  			replace:  func([]string, Attr) Attr { return Attr{} },
   479  			attrs:    []Attr{Group("", Int("c", 3))},
   480  			wantText: "",
   481  			wantJSON: `{}`,
   482  		},
   483  		{
   484  			name: "replace partial empty attrs 1",
   485  			with: func(h Handler) Handler {
   486  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   487  			},
   488  			replace: func(groups []string, attr Attr) Attr {
   489  				return removeKeys(TimeKey, LevelKey, MessageKey, "a")(groups, attr)
   490  			},
   491  			attrs:    []Attr{Group("i", Int("c", 3))},
   492  			wantText: "g.h.b=2 g.h.i.c=3",
   493  			wantJSON: `{"g":{"h":{"b":2,"i":{"c":3}}}}`,
   494  		},
   495  		{
   496  			name: "replace partial empty attrs 2",
   497  			with: func(h Handler) Handler {
   498  				return h.WithGroup("g").WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   499  			},
   500  			replace: func(groups []string, attr Attr) Attr {
   501  				return removeKeys(TimeKey, LevelKey, MessageKey, "a", "b")(groups, attr)
   502  			},
   503  			attrs:    []Attr{Group("i", Int("c", 3))},
   504  			wantText: "g.n=4 g.h.i.c=3",
   505  			wantJSON: `{"g":{"n":4,"h":{"i":{"c":3}}}}`,
   506  		},
   507  		{
   508  			name: "replace partial empty attrs 3",
   509  			with: func(h Handler) Handler {
   510  				return h.WithGroup("g").WithAttrs([]Attr{Int("x", 0)}).WithAttrs([]Attr{Int("a", 1)}).WithAttrs([]Attr{Int("n", 4)}).WithGroup("h").WithAttrs([]Attr{Int("b", 2)})
   511  			},
   512  			replace: func(groups []string, attr Attr) Attr {
   513  				return removeKeys(TimeKey, LevelKey, MessageKey, "a", "c")(groups, attr)
   514  			},
   515  			attrs:    []Attr{Group("i", Int("c", 3))},
   516  			wantText: "g.x=0 g.n=4 g.h.b=2",
   517  			wantJSON: `{"g":{"x":0,"n":4,"h":{"b":2}}}`,
   518  		},
   519  		{
   520  			name: "replace resolved group",
   521  			replace: func(groups []string, a Attr) Attr {
   522  				if a.Value.Kind() == KindGroup {
   523  					return Attr{"bad", IntValue(1)}
   524  				}
   525  				return removeKeys(TimeKey, LevelKey, MessageKey)(groups, a)
   526  			},
   527  			attrs:    []Attr{Any("name", logValueName{"Perry", "Platypus"})},
   528  			wantText: "name.first=Perry name.last=Platypus",
   529  			wantJSON: `{"name":{"first":"Perry","last":"Platypus"}}`,
   530  		},
   531  	} {
   532  		r := NewRecord(testTime, LevelInfo, "message", callerPC(2))
   533  		line := strconv.Itoa(r.source().Line)
   534  		r.AddAttrs(test.attrs...)
   535  		var buf bytes.Buffer
   536  		opts := HandlerOptions{ReplaceAttr: test.replace, AddSource: test.addSource}
   537  		t.Run(test.name, func(t *testing.T) {
   538  			for _, handler := range []struct {
   539  				name string
   540  				h    Handler
   541  				want string
   542  			}{
   543  				{"text", NewTextHandler(&buf, &opts), test.wantText},
   544  				{"json", NewJSONHandler(&buf, &opts), test.wantJSON},
   545  			} {
   546  				t.Run(handler.name, func(t *testing.T) {
   547  					h := handler.h
   548  					if test.with != nil {
   549  						h = test.with(h)
   550  					}
   551  					buf.Reset()
   552  					if err := h.Handle(nil, r); err != nil {
   553  						t.Fatal(err)
   554  					}
   555  					want := strings.ReplaceAll(handler.want, "$LINE", line)
   556  					got := strings.TrimSuffix(buf.String(), "\n")
   557  					if got != want {
   558  						t.Errorf("\ngot  %s\nwant %s\n", got, want)
   559  					}
   560  				})
   561  			}
   562  		})
   563  	}
   564  }
   565  
   566  // removeKeys returns a function suitable for HandlerOptions.ReplaceAttr
   567  // that removes all Attrs with the given keys.
   568  func removeKeys(keys ...string) func([]string, Attr) Attr {
   569  	return func(_ []string, a Attr) Attr {
   570  		for _, k := range keys {
   571  			if a.Key == k {
   572  				return Attr{}
   573  			}
   574  		}
   575  		return a
   576  	}
   577  }
   578  
   579  func upperCaseKey(_ []string, a Attr) Attr {
   580  	a.Key = strings.ToUpper(a.Key)
   581  	return a
   582  }
   583  
   584  type logValueName struct {
   585  	first, last string
   586  }
   587  
   588  func (n logValueName) LogValue() Value {
   589  	return GroupValue(
   590  		String("first", n.first),
   591  		String("last", n.last))
   592  }
   593  
   594  func TestHandlerEnabled(t *testing.T) {
   595  	levelVar := func(l Level) *LevelVar {
   596  		var al LevelVar
   597  		al.Set(l)
   598  		return &al
   599  	}
   600  
   601  	for _, test := range []struct {
   602  		leveler Leveler
   603  		want    bool
   604  	}{
   605  		{nil, true},
   606  		{LevelWarn, false},
   607  		{&LevelVar{}, true}, // defaults to Info
   608  		{levelVar(LevelWarn), false},
   609  		{LevelDebug, true},
   610  		{levelVar(LevelDebug), true},
   611  	} {
   612  		h := &commonHandler{opts: HandlerOptions{Level: test.leveler}}
   613  		got := h.enabled(LevelInfo)
   614  		if got != test.want {
   615  			t.Errorf("%v: got %t, want %t", test.leveler, got, test.want)
   616  		}
   617  	}
   618  }
   619  
   620  func TestSecondWith(t *testing.T) {
   621  	// Verify that a second call to Logger.With does not corrupt
   622  	// the original.
   623  	var buf bytes.Buffer
   624  	h := NewTextHandler(&buf, &HandlerOptions{ReplaceAttr: removeKeys(TimeKey)})
   625  	logger := New(h).With(
   626  		String("app", "playground"),
   627  		String("role", "tester"),
   628  		Int("data_version", 2),
   629  	)
   630  	appLogger := logger.With("type", "log") // this becomes type=met
   631  	_ = logger.With("type", "metric")
   632  	appLogger.Info("foo")
   633  	got := strings.TrimSpace(buf.String())
   634  	want := `level=INFO msg=foo app=playground role=tester data_version=2 type=log`
   635  	if got != want {
   636  		t.Errorf("\ngot  %s\nwant %s", got, want)
   637  	}
   638  }
   639  
   640  func TestReplaceAttrGroups(t *testing.T) {
   641  	// Verify that ReplaceAttr is called with the correct groups.
   642  	type ga struct {
   643  		groups string
   644  		key    string
   645  		val    string
   646  	}
   647  
   648  	var got []ga
   649  
   650  	h := NewTextHandler(io.Discard, &HandlerOptions{ReplaceAttr: func(gs []string, a Attr) Attr {
   651  		v := a.Value.String()
   652  		if a.Key == TimeKey {
   653  			v = "<now>"
   654  		}
   655  		got = append(got, ga{strings.Join(gs, ","), a.Key, v})
   656  		return a
   657  	}})
   658  	New(h).
   659  		With(Int("a", 1)).
   660  		WithGroup("g1").
   661  		With(Int("b", 2)).
   662  		WithGroup("g2").
   663  		With(
   664  			Int("c", 3),
   665  			Group("g3", Int("d", 4)),
   666  			Int("e", 5)).
   667  		Info("m",
   668  			Int("f", 6),
   669  			Group("g4", Int("h", 7)),
   670  			Int("i", 8))
   671  
   672  	want := []ga{
   673  		{"", "a", "1"},
   674  		{"g1", "b", "2"},
   675  		{"g1,g2", "c", "3"},
   676  		{"g1,g2,g3", "d", "4"},
   677  		{"g1,g2", "e", "5"},
   678  		{"", "time", "<now>"},
   679  		{"", "level", "INFO"},
   680  		{"", "msg", "m"},
   681  		{"g1,g2", "f", "6"},
   682  		{"g1,g2,g4", "h", "7"},
   683  		{"g1,g2", "i", "8"},
   684  	}
   685  	if !slices.Equal(got, want) {
   686  		t.Errorf("\ngot  %v\nwant %v", got, want)
   687  	}
   688  }
   689  
   690  const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
   691  
   692  func TestWriteTimeRFC3339(t *testing.T) {
   693  	for _, tm := range []time.Time{
   694  		time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC),
   695  		time.Date(2000, 1, 2, 3, 4, 5, 400, time.Local),
   696  		time.Date(2000, 11, 12, 3, 4, 500, 5e7, time.UTC),
   697  	} {
   698  		got := string(appendRFC3339Millis(nil, tm))
   699  		want := tm.Format(rfc3339Millis)
   700  		if got != want {
   701  			t.Errorf("got %s, want %s", got, want)
   702  		}
   703  	}
   704  }
   705  
   706  func BenchmarkWriteTime(b *testing.B) {
   707  	tm := time.Date(2022, 3, 4, 5, 6, 7, 823456789, time.Local)
   708  	b.ResetTimer()
   709  	var buf []byte
   710  	for i := 0; i < b.N; i++ {
   711  		buf = appendRFC3339Millis(buf[:0], tm)
   712  	}
   713  }
   714  

View as plain text