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.
18 type testCase struct {
19 // If non-empty, explanation explains the violated constraint.
21 // f executes a single log event using its argument logger.
22 // So that mkdescs.sh can generate the right description,
23 // the body of f must appear on a single line whose first
24 // non-whitespace characters are "l.".
26 // If mod is not nil, it is called to modify the Record
27 // generated by the Logger before it is passed to the Handler.
28 mod func(*slog.Record)
29 // checks is a list of checks to run on the result.
33 // TestHandler tests a [slog.Handler].
34 // If TestHandler finds any misbehaviors, it returns an error for each,
35 // combined into a single error with errors.Join.
37 // TestHandler installs the given Handler in a [slog.Logger] and
38 // makes several calls to the Logger's output methods.
40 // The results function is invoked after all such calls.
41 // It should return a slice of map[string]any, one for each call to a Logger output method.
42 // The keys and values of the map should correspond to the keys and values of the Handler's
43 // output. Each group in the output should be represented as its own nested map[string]any.
44 // The standard keys slog.TimeKey, slog.LevelKey and slog.MessageKey should be used.
46 // If the Handler outputs JSON, then calling [encoding/json.Unmarshal] with a `map[string]any`
47 // will create the right data structure.
49 // If a Handler intentionally drops an attribute that is checked by a test,
50 // then the results function should check for its absence and add it to the map it returns.
51 func TestHandler(h slog.Handler, results func() []map[string]any) error {
54 explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"),
55 f: func(l *slog.Logger) {
60 hasKey(slog.LevelKey),
61 hasAttr(slog.MessageKey, "message"),
65 explanation: withSource("a Handler should output attributes passed to the logging function"),
66 f: func(l *slog.Logger) {
67 l.Info("message", "k", "v")
74 explanation: withSource("a Handler should ignore an empty Attr"),
75 f: func(l *slog.Logger) {
76 l.Info("msg", "a", "b", "", nil, "c", "d")
85 explanation: withSource("a Handler should ignore a zero Record.Time"),
86 f: func(l *slog.Logger) {
87 l.Info("msg", "k", "v")
89 mod: func(r *slog.Record) { r.Time = time.Time{} },
91 missingKey(slog.TimeKey),
95 explanation: withSource("a Handler should include the attributes from the WithAttrs method"),
96 f: func(l *slog.Logger) {
97 l.With("a", "b").Info("msg", "k", "v")
105 explanation: withSource("a Handler should handle Group attributes"),
106 f: func(l *slog.Logger) {
107 l.Info("msg", "a", "b", slog.Group("G", slog.String("c", "d")), "e", "f")
111 inGroup("G", hasAttr("c", "d")),
116 explanation: withSource("a Handler should ignore an empty group"),
117 f: func(l *slog.Logger) {
118 l.Info("msg", "a", "b", slog.Group("G"), "e", "f")
127 explanation: withSource("a Handler should inline the Attrs of a group with an empty key"),
128 f: func(l *slog.Logger) {
129 l.Info("msg", "a", "b", slog.Group("", slog.String("c", "d")), "e", "f")
139 explanation: withSource("a Handler should handle the WithGroup method"),
140 f: func(l *slog.Logger) {
141 l.WithGroup("G").Info("msg", "a", "b")
144 hasKey(slog.TimeKey),
145 hasKey(slog.LevelKey),
146 hasAttr(slog.MessageKey, "msg"),
148 inGroup("G", hasAttr("a", "b")),
152 explanation: withSource("a Handler should handle multiple WithGroup and WithAttr calls"),
153 f: func(l *slog.Logger) {
154 l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg", "e", "f")
157 hasKey(slog.TimeKey),
158 hasKey(slog.LevelKey),
159 hasAttr(slog.MessageKey, "msg"),
161 inGroup("G", hasAttr("c", "d")),
162 inGroup("G", inGroup("H", hasAttr("e", "f"))),
166 explanation: withSource("a Handler should not output groups if there are no attributes"),
167 f: func(l *slog.Logger) {
168 l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg")
171 hasKey(slog.TimeKey),
172 hasKey(slog.LevelKey),
173 hasAttr(slog.MessageKey, "msg"),
175 inGroup("G", hasAttr("c", "d")),
176 inGroup("G", missingKey("H")),
180 explanation: withSource("a Handler should call Resolve on attribute values"),
181 f: func(l *slog.Logger) {
182 l.Info("msg", "k", &replace{"replaced"})
184 checks: []check{hasAttr("k", "replaced")},
187 explanation: withSource("a Handler should call Resolve on attribute values in groups"),
188 f: func(l *slog.Logger) {
191 slog.String("a", "v1"),
192 slog.Any("b", &replace{"v2"})))
195 inGroup("G", hasAttr("a", "v1")),
196 inGroup("G", hasAttr("b", "v2")),
200 explanation: withSource("a Handler should call Resolve on attribute values from WithAttrs"),
201 f: func(l *slog.Logger) {
202 l = l.With("k", &replace{"replaced"})
205 checks: []check{hasAttr("k", "replaced")},
208 explanation: withSource("a Handler should call Resolve on attribute values in groups from WithAttrs"),
209 f: func(l *slog.Logger) {
210 l = l.With(slog.Group("G",
211 slog.String("a", "v1"),
212 slog.Any("b", &replace{"v2"})))
216 inGroup("G", hasAttr("a", "v1")),
217 inGroup("G", hasAttr("b", "v2")),
222 // Run the handler on the test cases.
223 for _, c := range cases {
226 ht = &wrapper{h, c.mod}
232 // Collect and check the results.
235 if g, w := len(res), len(cases); g != w {
236 return fmt.Errorf("got %d results, want %d", g, w)
238 for i, got := range results() {
240 for _, check := range c.checks {
241 if p := check(got); p != "" {
242 errs = append(errs, fmt.Errorf("%s: %s", p, c.explanation))
246 return errors.Join(errs...)
249 type check func(map[string]any) string
251 func hasKey(key string) check {
252 return func(m map[string]any) string {
253 if _, ok := m[key]; !ok {
254 return fmt.Sprintf("missing key %q", key)
260 func missingKey(key string) check {
261 return func(m map[string]any) string {
262 if _, ok := m[key]; ok {
263 return fmt.Sprintf("unexpected key %q", key)
269 func hasAttr(key string, wantVal any) check {
270 return func(m map[string]any) string {
271 if s := hasKey(key)(m); s != "" {
275 if !reflect.DeepEqual(gotVal, wantVal) {
276 return fmt.Sprintf("%q: got %#v, want %#v", key, gotVal, wantVal)
282 func inGroup(name string, c check) check {
283 return func(m map[string]any) string {
286 return fmt.Sprintf("missing group %q", name)
288 g, ok := v.(map[string]any)
290 return fmt.Sprintf("value for group %q is not map[string]any", name)
296 type wrapper struct {
298 mod func(*slog.Record)
301 func (h *wrapper) Handle(ctx context.Context, r slog.Record) error {
303 return h.Handler.Handle(ctx, r)
306 func withSource(s string) string {
307 _, file, line, ok := runtime.Caller(1)
309 panic("runtime.Caller failed")
311 return fmt.Sprintf("%s (%s:%d)", s, file, line)
314 type replace struct {
318 func (r *replace) LogValue() slog.Value { return slog.AnyValue(r.v) }
320 func (r *replace) String() string {
321 return fmt.Sprintf("<replace(%v)>", r.v)