@@ -12,10 +12,13 @@ import (
12
12
"log/slog"
13
13
"reflect"
14
14
"runtime"
15
+ "testing"
15
16
"time"
16
17
)
17
18
18
19
type testCase struct {
20
+ // Subtest name.
21
+ name string
19
22
// If non-empty, explanation explains the violated constraint.
20
23
explanation string
21
24
// f executes a single log event using its argument logger.
@@ -30,6 +33,191 @@ type testCase struct {
30
33
checks []check
31
34
}
32
35
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
+
33
221
// TestHandler tests a [slog.Handler].
34
222
// If TestHandler finds any misbehaviors, it returns an error for each,
35
223
// combined into a single error with errors.Join.
@@ -50,176 +238,6 @@ type testCase struct {
50
238
// If a Handler intentionally drops an attribute that is checked by a test,
51
239
// then the results function should check for its absence and add it to the map it returns.
52
240
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
-
223
241
// Run the handler on the test cases.
224
242
for _ , c := range cases {
225
243
ht := h
@@ -239,14 +257,37 @@ func TestHandler(h slog.Handler, results func() []map[string]any) error {
239
257
for i , got := range results () {
240
258
c := cases [i ]
241
259
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 ))
244
262
}
245
263
}
246
264
}
247
265
return errors .Join (errs ... )
248
266
}
249
267
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
+
250
291
type check func (map [string ]any ) string
251
292
252
293
func hasKey (key string ) check {
0 commit comments