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
7package slog
8
9import (
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
23func 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
109func 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.
156func 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.
568func 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
579func upperCaseKey(_ []string, a Attr) Attr {
580	a.Key = strings.ToUpper(a.Key)
581	return a
582}
583
584type logValueName struct {
585	first, last string
586}
587
588func (n logValueName) LogValue() Value {
589	return GroupValue(
590		String("first", n.first),
591		String("last", n.last))
592}
593
594func 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
620func 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
640func 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
690const rfc3339Millis = "2006-01-02T15:04:05.000Z07:00"
691
692func 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
706func 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