1 // Copyright 2023 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.
5 // Package slogtest implements support for testing implementations of log/slog.Handler.
19 type testCase struct {
22 // If non-empty, explanation explains the violated constraint.
24 // f executes a single log event using its argument logger.
25 // So that mkdescs.sh can generate the right description,
26 // the body of f must appear on a single line whose first
27 // non-whitespace characters are "l.".
29 // If mod is not nil, it is called to modify the Record
30 // generated by the Logger before it is passed to the Handler.
31 mod func(*slog.Record)
32 // checks is a list of checks to run on the result.
36 var cases = []testCase{
39 explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"),
40 f: func(l *slog.Logger) {
45 hasKey(slog.LevelKey),
46 hasAttr(slog.MessageKey, "message"),
51 explanation: withSource("a Handler should output attributes passed to the logging function"),
52 f: func(l *slog.Logger) {
53 l.Info("message", "k", "v")
61 explanation: withSource("a Handler should ignore an empty Attr"),
62 f: func(l *slog.Logger) {
63 l.Info("msg", "a", "b", "", nil, "c", "d")
73 explanation: withSource("a Handler should ignore a zero Record.Time"),
74 f: func(l *slog.Logger) {
75 l.Info("msg", "k", "v")
77 mod: func(r *slog.Record) { r.Time = time.Time{} },
79 missingKey(slog.TimeKey),
84 explanation: withSource("a Handler should include the attributes from the WithAttrs method"),
85 f: func(l *slog.Logger) {
86 l.With("a", "b").Info("msg", "k", "v")
95 explanation: withSource("a Handler should handle Group attributes"),
96 f: func(l *slog.Logger) {
97 l.Info("msg", "a", "b", slog.Group("G", slog.String("c", "d")), "e", "f")
101 inGroup("G", hasAttr("c", "d")),
107 explanation: withSource("a Handler should ignore an empty group"),
108 f: func(l *slog.Logger) {
109 l.Info("msg", "a", "b", slog.Group("G"), "e", "f")
118 name: "inline-group",
119 explanation: withSource("a Handler should inline the Attrs of a group with an empty key"),
120 f: func(l *slog.Logger) {
121 l.Info("msg", "a", "b", slog.Group("", slog.String("c", "d")), "e", "f")
132 explanation: withSource("a Handler should handle the WithGroup method"),
133 f: func(l *slog.Logger) {
134 l.WithGroup("G").Info("msg", "a", "b")
137 hasKey(slog.TimeKey),
138 hasKey(slog.LevelKey),
139 hasAttr(slog.MessageKey, "msg"),
141 inGroup("G", hasAttr("a", "b")),
146 explanation: withSource("a Handler should handle multiple WithGroup and WithAttr calls"),
147 f: func(l *slog.Logger) {
148 l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg", "e", "f")
151 hasKey(slog.TimeKey),
152 hasKey(slog.LevelKey),
153 hasAttr(slog.MessageKey, "msg"),
155 inGroup("G", hasAttr("c", "d")),
156 inGroup("G", inGroup("H", hasAttr("e", "f"))),
160 name: "empty-group-record",
161 explanation: withSource("a Handler should not output groups if there are no attributes"),
162 f: func(l *slog.Logger) {
163 l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg")
166 hasKey(slog.TimeKey),
167 hasKey(slog.LevelKey),
168 hasAttr(slog.MessageKey, "msg"),
170 inGroup("G", hasAttr("c", "d")),
171 inGroup("G", missingKey("H")),
176 explanation: withSource("a Handler should call Resolve on attribute values"),
177 f: func(l *slog.Logger) {
178 l.Info("msg", "k", &replace{"replaced"})
180 checks: []check{hasAttr("k", "replaced")},
183 name: "resolve-groups",
184 explanation: withSource("a Handler should call Resolve on attribute values in groups"),
185 f: func(l *slog.Logger) {
188 slog.String("a", "v1"),
189 slog.Any("b", &replace{"v2"})))
192 inGroup("G", hasAttr("a", "v1")),
193 inGroup("G", hasAttr("b", "v2")),
197 name: "resolve-WithAttrs",
198 explanation: withSource("a Handler should call Resolve on attribute values from WithAttrs"),
199 f: func(l *slog.Logger) {
200 l = l.With("k", &replace{"replaced"})
203 checks: []check{hasAttr("k", "replaced")},
206 name: "resolve-WithAttrs-groups",
207 explanation: withSource("a Handler should call Resolve on attribute values in groups from WithAttrs"),
208 f: func(l *slog.Logger) {
209 l = l.With(slog.Group("G",
210 slog.String("a", "v1"),
211 slog.Any("b", &replace{"v2"})))
215 inGroup("G", hasAttr("a", "v1")),
216 inGroup("G", hasAttr("b", "v2")),
221 explanation: withSource("a Handler should not output SourceKey if the PC is zero"),
222 f: func(l *slog.Logger) {
225 mod: func(r *slog.Record) { r.PC = 0 },
227 missingKey(slog.SourceKey),
232 // TestHandler tests a [slog.Handler].
233 // If TestHandler finds any misbehaviors, it returns an error for each,
234 // combined into a single error with errors.Join.
236 // TestHandler installs the given Handler in a [slog.Logger] and
237 // makes several calls to the Logger's output methods.
238 // The Handler should be enabled for levels Info and above.
240 // The results function is invoked after all such calls.
241 // It should return a slice of map[string]any, one for each call to a Logger output method.
242 // The keys and values of the map should correspond to the keys and values of the Handler's
243 // output. Each group in the output should be represented as its own nested map[string]any.
244 // The standard keys slog.TimeKey, slog.LevelKey and slog.MessageKey should be used.
246 // If the Handler outputs JSON, then calling [encoding/json.Unmarshal] with a `map[string]any`
247 // will create the right data structure.
249 // If a Handler intentionally drops an attribute that is checked by a test,
250 // then the results function should check for its absence and add it to the map it returns.
251 func TestHandler(h slog.Handler, results func() []map[string]any) error {
252 // Run the handler on the test cases.
253 for _, c := range cases {
256 ht = &wrapper{h, c.mod}
262 // Collect and check the results.
265 if g, w := len(res), len(cases); g != w {
266 return fmt.Errorf("got %d results, want %d", g, w)
268 for i, got := range results() {
270 for _, check := range c.checks {
271 if problem := check(got); problem != "" {
272 errs = append(errs, fmt.Errorf("%s: %s", problem, c.explanation))
276 return errors.Join(errs...)
279 // Run exercises a [slog.Handler] on the same test cases as [TestHandler], but
280 // runs each case in a subtest. For each test case, it first calls newHandler to
281 // get an instance of the handler under test, then runs the test case, then
282 // calls result to get the result. If the test case fails, it calls t.Error.
283 func Run(t *testing.T, newHandler func(*testing.T) slog.Handler, result func(*testing.T) map[string]any) {
284 for _, c := range cases {
285 t.Run(c.name, func(t *testing.T) {
288 h = &wrapper{h, c.mod}
293 for _, check := range c.checks {
294 if p := check(got); p != "" {
295 t.Errorf("%s: %s", p, c.explanation)
302 type check func(map[string]any) string
304 func hasKey(key string) check {
305 return func(m map[string]any) string {
306 if _, ok := m[key]; !ok {
307 return fmt.Sprintf("missing key %q", key)
313 func missingKey(key string) check {
314 return func(m map[string]any) string {
315 if _, ok := m[key]; ok {
316 return fmt.Sprintf("unexpected key %q", key)
322 func hasAttr(key string, wantVal any) check {
323 return func(m map[string]any) string {
324 if s := hasKey(key)(m); s != "" {
328 if !reflect.DeepEqual(gotVal, wantVal) {
329 return fmt.Sprintf("%q: got %#v, want %#v", key, gotVal, wantVal)
335 func inGroup(name string, c check) check {
336 return func(m map[string]any) string {
339 return fmt.Sprintf("missing group %q", name)
341 g, ok := v.(map[string]any)
343 return fmt.Sprintf("value for group %q is not map[string]any", name)
349 type wrapper struct {
351 mod func(*slog.Record)
354 func (h *wrapper) Handle(ctx context.Context, r slog.Record) error {
356 return h.Handler.Handle(ctx, r)
359 func withSource(s string) string {
360 _, file, line, ok := runtime.Caller(1)
362 panic("runtime.Caller failed")
364 return fmt.Sprintf("%s (%s:%d)", s, file, line)
367 type replace struct {
371 func (r *replace) LogValue() slog.Value { return slog.AnyValue(r.v) }
373 func (r *replace) String() string {
374 return fmt.Sprintf("<replace(%v)>", r.v)