1 package slogtest
2
3 import (
4 "context"
5 "fmt"
6 "reflect"
7 "runtime"
8 "time"
9
10 "golang.org/x/exp/slog"
11 )
12
13 type testCase struct {
14
15 explanation string
16
17
18
19
20 f func(*slog.Logger)
21
22
23 mod func(*slog.Record)
24
25 checks []check
26 }
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 func TestHandler(h slog.Handler, results func() []map[string]any) error {
47 cases := []testCase{
48 {
49 explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"),
50 f: func(l *slog.Logger) {
51 l.Info("message")
52 },
53 checks: []check{
54 hasKey(slog.TimeKey),
55 hasKey(slog.LevelKey),
56 hasAttr(slog.MessageKey, "message"),
57 },
58 },
59 {
60 explanation: withSource("a Handler should output attributes passed to the logging function"),
61 f: func(l *slog.Logger) {
62 l.Info("message", "k", "v")
63 },
64 checks: []check{
65 hasAttr("k", "v"),
66 },
67 },
68 {
69 explanation: withSource("a Handler should ignore an empty Attr"),
70 f: func(l *slog.Logger) {
71 l.Info("msg", "a", "b", "", nil, "c", "d")
72 },
73 checks: []check{
74 hasAttr("a", "b"),
75 missingKey(""),
76 hasAttr("c", "d"),
77 },
78 },
79 {
80 explanation: withSource("a Handler should ignore a zero Record.Time"),
81 f: func(l *slog.Logger) {
82 l.Info("msg", "k", "v")
83 },
84 mod: func(r *slog.Record) { r.Time = time.Time{} },
85 checks: []check{
86 missingKey(slog.TimeKey),
87 },
88 },
89 {
90 explanation: withSource("a Handler should include the attributes from the WithAttrs method"),
91 f: func(l *slog.Logger) {
92 l.With("a", "b").Info("msg", "k", "v")
93 },
94 checks: []check{
95 hasAttr("a", "b"),
96 hasAttr("k", "v"),
97 },
98 },
99 {
100 explanation: withSource("a Handler should handle Group attributes"),
101 f: func(l *slog.Logger) {
102 l.Info("msg", "a", "b", slog.Group("G", slog.String("c", "d")), "e", "f")
103 },
104 checks: []check{
105 hasAttr("a", "b"),
106 inGroup("G", hasAttr("c", "d")),
107 hasAttr("e", "f"),
108 },
109 },
110 {
111 explanation: withSource("a Handler should ignore an empty group"),
112 f: func(l *slog.Logger) {
113 l.Info("msg", "a", "b", slog.Group("G"), "e", "f")
114 },
115 checks: []check{
116 hasAttr("a", "b"),
117 missingKey("G"),
118 hasAttr("e", "f"),
119 },
120 },
121 {
122 explanation: withSource("a Handler should inline the Attrs of a group with an empty key"),
123 f: func(l *slog.Logger) {
124 l.Info("msg", "a", "b", slog.Group("", slog.String("c", "d")), "e", "f")
125
126 },
127 checks: []check{
128 hasAttr("a", "b"),
129 hasAttr("c", "d"),
130 hasAttr("e", "f"),
131 },
132 },
133 {
134 explanation: withSource("a Handler should handle the WithGroup method"),
135 f: func(l *slog.Logger) {
136 l.WithGroup("G").Info("msg", "a", "b")
137 },
138 checks: []check{
139 hasKey(slog.TimeKey),
140 hasKey(slog.LevelKey),
141 hasAttr(slog.MessageKey, "msg"),
142 missingKey("a"),
143 inGroup("G", hasAttr("a", "b")),
144 },
145 },
146 {
147 explanation: withSource("a Handler should handle multiple WithGroup and WithAttr calls"),
148 f: func(l *slog.Logger) {
149 l.With("a", "b").WithGroup("G").With("c", "d").WithGroup("H").Info("msg", "e", "f")
150 },
151 checks: []check{
152 hasKey(slog.TimeKey),
153 hasKey(slog.LevelKey),
154 hasAttr(slog.MessageKey, "msg"),
155 hasAttr("a", "b"),
156 inGroup("G", hasAttr("c", "d")),
157 inGroup("G", inGroup("H", hasAttr("e", "f"))),
158 },
159 },
160 {
161 explanation: withSource("a Handler should call Resolve on attribute values"),
162 f: func(l *slog.Logger) {
163 l.Info("msg", "k", &replace{"replaced"})
164 },
165 checks: []check{hasAttr("k", "replaced")},
166 },
167 {
168 explanation: withSource("a Handler should call Resolve on attribute values in groups"),
169 f: func(l *slog.Logger) {
170 l.Info("msg",
171 slog.Group("G",
172 slog.String("a", "v1"),
173 slog.Any("b", &replace{"v2"})))
174 },
175 checks: []check{
176 inGroup("G", hasAttr("a", "v1")),
177 inGroup("G", hasAttr("b", "v2")),
178 },
179 },
180 {
181 explanation: withSource("a Handler should call Resolve on attribute values from WithAttrs"),
182 f: func(l *slog.Logger) {
183 l = l.With("k", &replace{"replaced"})
184 l.Info("msg")
185 },
186 checks: []check{hasAttr("k", "replaced")},
187 },
188 {
189 explanation: withSource("a Handler should call Resolve on attribute values in groups from WithAttrs"),
190 f: func(l *slog.Logger) {
191 l = l.With(slog.Group("G",
192 slog.String("a", "v1"),
193 slog.Any("b", &replace{"v2"})))
194 l.Info("msg")
195 },
196 checks: []check{
197 inGroup("G", hasAttr("a", "v1")),
198 inGroup("G", hasAttr("b", "v2")),
199 },
200 },
201 }
202
203
204 for _, c := range cases {
205 ht := h
206 if c.mod != nil {
207 ht = &wrapper{h, c.mod}
208 }
209 l := slog.New(ht)
210 c.f(l)
211 }
212
213
214 var errs []error
215 res := results()
216 if g, w := len(res), len(cases); g != w {
217 return fmt.Errorf("got %d results, want %d", g, w)
218 }
219 for i, got := range results() {
220 c := cases[i]
221 for _, check := range c.checks {
222 if p := check(got); p != "" {
223 errs = append(errs, fmt.Errorf("%s: %s", p, c.explanation))
224 }
225 }
226 }
227 return errorsJoin(errs...)
228 }
229
230 type check func(map[string]any) string
231
232 func hasKey(key string) check {
233 return func(m map[string]any) string {
234 if _, ok := m[key]; !ok {
235 return fmt.Sprintf("missing key %q", key)
236 }
237 return ""
238 }
239 }
240
241 func missingKey(key string) check {
242 return func(m map[string]any) string {
243 if _, ok := m[key]; ok {
244 return fmt.Sprintf("unexpected key %q", key)
245 }
246 return ""
247 }
248 }
249
250 func hasAttr(key string, wantVal any) check {
251 return func(m map[string]any) string {
252 if s := hasKey(key)(m); s != "" {
253 return s
254 }
255 gotVal := m[key]
256 if !reflect.DeepEqual(gotVal, wantVal) {
257 return fmt.Sprintf("%q: got %#v, want %#v", key, gotVal, wantVal)
258 }
259 return ""
260 }
261 }
262
263 func inGroup(name string, c check) check {
264 return func(m map[string]any) string {
265 v, ok := m[name]
266 if !ok {
267 return fmt.Sprintf("missing group %q", name)
268 }
269 g, ok := v.(map[string]any)
270 if !ok {
271 return fmt.Sprintf("value for group %q is not map[string]any", name)
272 }
273 return c(g)
274 }
275 }
276
277 type wrapper struct {
278 slog.Handler
279 mod func(*slog.Record)
280 }
281
282 func (h *wrapper) Handle(ctx context.Context, r slog.Record) error {
283 h.mod(&r)
284 return h.Handler.Handle(ctx, r)
285 }
286
287 func withSource(s string) string {
288 _, file, line, ok := runtime.Caller(1)
289 if !ok {
290 panic("runtime.Caller failed")
291 }
292 return fmt.Sprintf("%s (%s:%d)", s, file, line)
293 }
294
295 type replace struct {
296 v any
297 }
298
299 func (r *replace) LogValue() slog.Value { return slog.AnyValue(r.v) }
300
301 func (r *replace) String() string {
302 return fmt.Sprintf("<replace(%v)>", r.v)
303 }
304
View as plain text