Skip to content

Commit a742ae4

Browse files
committedSep 8, 2023
testing/slogtest: add Run to run cases as subtests
This is an implementation of proposal #61758. It adds a function to slogtest that runs each test case in a subtest, instead of running them all at once. That allows the caller to control which cases are run. Fixes #61706. Fixes #61758. Change-Id: I95108b7b753675203ca7f0f00ccbc242bd9c2a9f Reviewed-on: https://go-review.googlesource.com/c/go/+/516076 Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]>
1 parent af3bf86 commit a742ae4

File tree

4 files changed

+248
-175
lines changed

4 files changed

+248
-175
lines changed
 

Diff for: ‎api/next/61758.txt

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pkg testing/slogtest, func Run(*testing.T, func(*testing.T) slog.Handler, func(*testing.T) map[string]interface{}) #61758

Diff for: ‎src/go/build/deps_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -572,15 +572,15 @@ var depsRules = `
572572
< testing/iotest
573573
< testing/fstest;
574574
575-
log/slog
576-
< testing/slogtest;
577-
578575
FMT, flag, math/rand
579576
< testing/quick;
580577
581578
FMT, DEBUG, flag, runtime/trace, internal/sysinfo, math/rand
582579
< testing;
583580
581+
log/slog, testing
582+
< testing/slogtest;
583+
584584
FMT, crypto/sha256, encoding/json, go/ast, go/parser, go/token,
585585
internal/godebug, math/rand, encoding/hex, crypto/sha256
586586
< internal/fuzz;

Diff for: ‎src/testing/slogtest/run_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.
4+
5+
package slogtest_test
6+
7+
import (
8+
"bytes"
9+
"encoding/json"
10+
"log/slog"
11+
"testing"
12+
"testing/slogtest"
13+
)
14+
15+
func TestRun(t *testing.T) {
16+
var buf bytes.Buffer
17+
18+
newHandler := func(*testing.T) slog.Handler {
19+
buf.Reset()
20+
return slog.NewJSONHandler(&buf, nil)
21+
}
22+
result := func(t *testing.T) map[string]any {
23+
m := map[string]any{}
24+
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
25+
t.Fatal(err)
26+
}
27+
return m
28+
}
29+
30+
slogtest.Run(t, newHandler, result)
31+
}

Diff for: ‎src/testing/slogtest/slogtest.go

+213-172
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@ import (
1212
"log/slog"
1313
"reflect"
1414
"runtime"
15+
"testing"
1516
"time"
1617
)
1718

1819
type testCase struct {
20+
// Subtest name.
21+
name string
1922
// If non-empty, explanation explains the violated constraint.
2023
explanation string
2124
// f executes a single log event using its argument logger.
@@ -30,6 +33,191 @@ type testCase struct {
3033
checks []check
3134
}
3235

36+
var cases = []testCase{
37+
{
38+
name: "built-ins",
39+
explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"),
40+
f: func(l *slog.Logger) {
41+
l.Info("message")
42+
},
43+
checks: []check{
44+
hasKey(slog.TimeKey),
45+
hasKey(slog.LevelKey),
46+
hasAttr(slog.MessageKey, "message"),
47+
},
48+
},
49+
{
50+
name: "attrs",
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")
54+
},
55+
checks: []check{
56+
hasAttr("k", "v"),
57+
},
58+
},
59+
{
60+
name: "empty-attr",
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")
64+
},
65+
checks: []check{
66+
hasAttr("a", "b"),
67+
missingKey(""),
68+
hasAttr("c", "d"),
69+
},
70+
},
71+
{
72+
name: "zero-time",
73+
explanation: withSource("a Handler should ignore a zero Record.Time"),
74+
f: func(l *slog.Logger) {
75+
l.Info("msg", "k", "v")
76+
},
77+
mod: func(r *slog.Record) { r.Time = time.Time{} },
78+
checks: []check{
79+
missingKey(slog.TimeKey),
80+
},
81+
},
82+
{
83+
name: "WithAttrs",
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")
87+
},
88+
checks: []check{
89+
hasAttr("a", "b"),
90+
hasAttr("k", "v"),
91+
},
92+
},
93+
{
94+
name: "groups",
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")
98+
},
99+
checks: []check{
100+
hasAttr("a", "b"),
101+
inGroup("G", hasAttr("c", "d")),
102+
hasAttr("e", "f"),
103+
},
104+
},
105+
{
106+
name: "empty-group",
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")
110+
},
111+
checks: []check{
112+
hasAttr("a", "b"),
113+
missingKey("G"),
114+
hasAttr("e", "f"),
115+
},
116+
},
117+
{
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")
122+
123+
},
124+
checks: []check{
125+
hasAttr("a", "b"),
126+
hasAttr("c", "d"),
127+
hasAttr("e", "f"),
128+
},
129+
},
130+
{
131+
name: "WithGroup",
132+
explanation: withSource("a Handler should handle the WithGroup method"),
133+
f: func(l *slog.Logger) {
134+
l.WithGroup("G").Info("msg", "a", "b")
135+
},
136+
checks: []check{
137+
hasKey(slog.TimeKey),
138+
hasKey(slog.LevelKey),
139+
hasAttr(slog.MessageKey, "msg"),
140+
missingKey("a"),
141+
inGroup("G", hasAttr("a", "b")),
142+
},
143+
},
144+
{
145+
name: "multi-With",
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")
149+
},
150+
checks: []check{
151+
hasKey(slog.TimeKey),
152+
hasKey(slog.LevelKey),
153+
hasAttr(slog.MessageKey, "msg"),
154+
hasAttr("a", "b"),
155+
inGroup("G", hasAttr("c", "d")),
156+
inGroup("G", inGroup("H", hasAttr("e", "f"))),
157+
},
158+
},
159+
{
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")
164+
},
165+
checks: []check{
166+
hasKey(slog.TimeKey),
167+
hasKey(slog.LevelKey),
168+
hasAttr(slog.MessageKey, "msg"),
169+
hasAttr("a", "b"),
170+
inGroup("G", hasAttr("c", "d")),
171+
inGroup("G", missingKey("H")),
172+
},
173+
},
174+
{
175+
name: "resolve",
176+
explanation: withSource("a Handler should call Resolve on attribute values"),
177+
f: func(l *slog.Logger) {
178+
l.Info("msg", "k", &replace{"replaced"})
179+
},
180+
checks: []check{hasAttr("k", "replaced")},
181+
},
182+
{
183+
name: "resolve-groups",
184+
explanation: withSource("a Handler should call Resolve on attribute values in groups"),
185+
f: func(l *slog.Logger) {
186+
l.Info("msg",
187+
slog.Group("G",
188+
slog.String("a", "v1"),
189+
slog.Any("b", &replace{"v2"})))
190+
},
191+
checks: []check{
192+
inGroup("G", hasAttr("a", "v1")),
193+
inGroup("G", hasAttr("b", "v2")),
194+
},
195+
},
196+
{
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"})
201+
l.Info("msg")
202+
},
203+
checks: []check{hasAttr("k", "replaced")},
204+
},
205+
{
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"})))
212+
l.Info("msg")
213+
},
214+
checks: []check{
215+
inGroup("G", hasAttr("a", "v1")),
216+
inGroup("G", hasAttr("b", "v2")),
217+
},
218+
},
219+
}
220+
33221
// TestHandler tests a [slog.Handler].
34222
// If TestHandler finds any misbehaviors, it returns an error for each,
35223
// combined into a single error with errors.Join.
@@ -50,176 +238,6 @@ type testCase struct {
50238
// If a Handler intentionally drops an attribute that is checked by a test,
51239
// then the results function should check for its absence and add it to the map it returns.
52240
func TestHandler(h slog.Handler, results func() []map[string]any) error {
53-
cases := []testCase{
54-
{
55-
explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"),
56-
f: func(l *slog.Logger) {
57-
l.Info("message")
58-
},
59-
checks: []check{
60-
hasKey(slog.TimeKey),
61-
hasKey(slog.LevelKey),
62-
hasAttr(slog.MessageKey, "message"),
63-
},
64-
},
65-
{
66-
explanation: withSource("a Handler should output attributes passed to the logging function"),
67-
f: func(l *slog.Logger) {
68-
l.Info("message", "k", "v")
69-
},
70-
checks: []check{
71-
hasAttr("k", "v"),
72-
},
73-
},
74-
{
75-
explanation: withSource("a Handler should ignore an empty Attr"),
76-
f: func(l *slog.Logger) {
77-
l.Info("msg", "a", "b", "", nil, "c", "d")
78-
},
79-
checks: []check{
80-
hasAttr("a", "b"),
81-
missingKey(""),
82-
hasAttr("c", "d"),
83-
},
84-
},
85-
{
86-
explanation: withSource("a Handler should ignore a zero Record.Time"),
87-
f: func(l *slog.Logger) {
88-
l.Info("msg", "k", "v")
89-
},
90-
mod: func(r *slog.Record) { r.Time = time.Time{} },
91-
checks: []check{
92-
missingKey(slog.TimeKey),
93-
},
94-
},
95-
{
96-
explanation: withSource("a Handler should include the attributes from the WithAttrs method"),
97-
f: func(l *slog.Logger) {
98-
l.With("a", "b").Info("msg", "k", "v")
99-
},
100-
checks: []check{
101-
hasAttr("a", "b"),
102-
hasAttr("k", "v"),
103-
},
104-
},
105-
{
106-
explanation: withSource("a Handler should handle Group attributes"),
107-
f: func(l *slog.Logger) {
108-
l.Info("msg", "a", "b", slog.Group("G", slog.String("c", "d")), "e", "f")
109-
},
110-
checks: []check{
111-
hasAttr("a", "b"),
112-
inGroup("G", hasAttr("c", "d")),
113-
hasAttr("e", "f"),
114-
},
115-
},
116-
{
117-
explanation: withSource("a Handler should ignore an empty group"),
118-
f: func(l *slog.Logger) {
119-
l.Info("msg", "a", "b", slog.Group("G"), "e", "f")
120-
},
121-
checks: []check{
122-
hasAttr("a", "b"),
123-
missingKey("G"),
124-
hasAttr("e", "f"),
125-
},
126-
},
127-
{
128-
explanation: withSource("a Handler should inline the Attrs of a group with an empty key"),
129-
f: func(l *slog.Logger) {
130-
l.Info("msg", "a", "b", slog.Group("", slog.String("c", "d")), "e", "f")
131-
132-
},
133-
checks: []check{
134-
hasAttr("a", "b"),
135-
hasAttr("c", "d"),
136-
hasAttr("e", "f"),
137-
},
138-
},
139-
{
140-
explanation: withSource("a Handler should handle the WithGroup method"),
141-
f: func(l *slog.Logger) {
142-
l.WithGroup("G").Info("msg", "a", "b")
143-
},
144-
checks: []check{
145-
hasKey(slog.TimeKey),
146-
hasKey(slog.LevelKey),
147-
hasAttr(slog.MessageKey, "msg"),
148-
missingKey("a"),
149-
inGroup("G", hasAttr("a", "b")),
150-
},
151-
},
152-
{
153-
explanation: withSource("a Handler should handle multiple WithGroup and WithAttr calls"),
154-
f: func(l *slog.Logger) {
155-
l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg", "e", "f")
156-
},
157-
checks: []check{
158-
hasKey(slog.TimeKey),
159-
hasKey(slog.LevelKey),
160-
hasAttr(slog.MessageKey, "msg"),
161-
hasAttr("a", "b"),
162-
inGroup("G", hasAttr("c", "d")),
163-
inGroup("G", inGroup("H", hasAttr("e", "f"))),
164-
},
165-
},
166-
{
167-
explanation: withSource("a Handler should not output groups if there are no attributes"),
168-
f: func(l *slog.Logger) {
169-
l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg")
170-
},
171-
checks: []check{
172-
hasKey(slog.TimeKey),
173-
hasKey(slog.LevelKey),
174-
hasAttr(slog.MessageKey, "msg"),
175-
hasAttr("a", "b"),
176-
inGroup("G", hasAttr("c", "d")),
177-
inGroup("G", missingKey("H")),
178-
},
179-
},
180-
{
181-
explanation: withSource("a Handler should call Resolve on attribute values"),
182-
f: func(l *slog.Logger) {
183-
l.Info("msg", "k", &replace{"replaced"})
184-
},
185-
checks: []check{hasAttr("k", "replaced")},
186-
},
187-
{
188-
explanation: withSource("a Handler should call Resolve on attribute values in groups"),
189-
f: func(l *slog.Logger) {
190-
l.Info("msg",
191-
slog.Group("G",
192-
slog.String("a", "v1"),
193-
slog.Any("b", &replace{"v2"})))
194-
},
195-
checks: []check{
196-
inGroup("G", hasAttr("a", "v1")),
197-
inGroup("G", hasAttr("b", "v2")),
198-
},
199-
},
200-
{
201-
explanation: withSource("a Handler should call Resolve on attribute values from WithAttrs"),
202-
f: func(l *slog.Logger) {
203-
l = l.With("k", &replace{"replaced"})
204-
l.Info("msg")
205-
},
206-
checks: []check{hasAttr("k", "replaced")},
207-
},
208-
{
209-
explanation: withSource("a Handler should call Resolve on attribute values in groups from WithAttrs"),
210-
f: func(l *slog.Logger) {
211-
l = l.With(slog.Group("G",
212-
slog.String("a", "v1"),
213-
slog.Any("b", &replace{"v2"})))
214-
l.Info("msg")
215-
},
216-
checks: []check{
217-
inGroup("G", hasAttr("a", "v1")),
218-
inGroup("G", hasAttr("b", "v2")),
219-
},
220-
},
221-
}
222-
223241
// Run the handler on the test cases.
224242
for _, c := range cases {
225243
ht := h
@@ -239,14 +257,37 @@ func TestHandler(h slog.Handler, results func() []map[string]any) error {
239257
for i, got := range results() {
240258
c := cases[i]
241259
for _, check := range c.checks {
242-
if p := check(got); p != "" {
243-
errs = append(errs, fmt.Errorf("%s: %s", p, c.explanation))
260+
if problem := check(got); problem != "" {
261+
errs = append(errs, fmt.Errorf("%s: %s", problem, c.explanation))
244262
}
245263
}
246264
}
247265
return errors.Join(errs...)
248266
}
249267

268+
// Run exercises a [slog.Handler] on the same test cases as [TestHandler], but
269+
// runs each case in a subtest. For each test case, it first calls newHandler to
270+
// get an instance of the handler under test, then runs the test case, then
271+
// calls result to get the result. If the test case fails, it calls t.Error.
272+
func Run(t *testing.T, newHandler func(*testing.T) slog.Handler, result func(*testing.T) map[string]any) {
273+
for _, c := range cases {
274+
t.Run(c.name, func(t *testing.T) {
275+
h := newHandler(t)
276+
if c.mod != nil {
277+
h = &wrapper{h, c.mod}
278+
}
279+
l := slog.New(h)
280+
c.f(l)
281+
got := result(t)
282+
for _, check := range c.checks {
283+
if p := check(got); p != "" {
284+
t.Errorf("%s: %s", p, c.explanation)
285+
}
286+
}
287+
})
288+
}
289+
}
290+
250291
type check func(map[string]any) string
251292

252293
func hasKey(key string) check {

0 commit comments

Comments
 (0)
Please sign in to comment.