github.com/AndrienkoAleksandr/go@v0.0.19/src/testing/slogtest/slogtest.go (about) 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 implements support for testing implementations of log/slog.Handler. 6 package slogtest 7 8 import ( 9 "context" 10 "errors" 11 "fmt" 12 "log/slog" 13 "reflect" 14 "runtime" 15 "time" 16 ) 17 18 type testCase struct { 19 // If non-empty, explanation explains the violated constraint. 20 explanation string 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.". 25 f func(*slog.Logger) 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. 30 checks []check 31 } 32 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. 36 // 37 // TestHandler installs the given Handler in a [slog.Logger] and 38 // makes several calls to the Logger's output methods. 39 // 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. 45 // 46 // If the Handler outputs JSON, then calling [encoding/json.Unmarshal] with a `map[string]any` 47 // will create the right data structure. 48 // 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 { 52 cases := []testCase{ 53 { 54 explanation: withSource("this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey"), 55 f: func(l *slog.Logger) { 56 l.Info("message") 57 }, 58 checks: []check{ 59 hasKey(slog.TimeKey), 60 hasKey(slog.LevelKey), 61 hasAttr(slog.MessageKey, "message"), 62 }, 63 }, 64 { 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") 68 }, 69 checks: []check{ 70 hasAttr("k", "v"), 71 }, 72 }, 73 { 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") 77 }, 78 checks: []check{ 79 hasAttr("a", "b"), 80 missingKey(""), 81 hasAttr("c", "d"), 82 }, 83 }, 84 { 85 explanation: withSource("a Handler should ignore a zero Record.Time"), 86 f: func(l *slog.Logger) { 87 l.Info("msg", "k", "v") 88 }, 89 mod: func(r *slog.Record) { r.Time = time.Time{} }, 90 checks: []check{ 91 missingKey(slog.TimeKey), 92 }, 93 }, 94 { 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") 98 }, 99 checks: []check{ 100 hasAttr("a", "b"), 101 hasAttr("k", "v"), 102 }, 103 }, 104 { 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") 108 }, 109 checks: []check{ 110 hasAttr("a", "b"), 111 inGroup("G", hasAttr("c", "d")), 112 hasAttr("e", "f"), 113 }, 114 }, 115 { 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") 119 }, 120 checks: []check{ 121 hasAttr("a", "b"), 122 missingKey("G"), 123 hasAttr("e", "f"), 124 }, 125 }, 126 { 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") 130 131 }, 132 checks: []check{ 133 hasAttr("a", "b"), 134 hasAttr("c", "d"), 135 hasAttr("e", "f"), 136 }, 137 }, 138 { 139 explanation: withSource("a Handler should handle the WithGroup method"), 140 f: func(l *slog.Logger) { 141 l.WithGroup("G").Info("msg", "a", "b") 142 }, 143 checks: []check{ 144 hasKey(slog.TimeKey), 145 hasKey(slog.LevelKey), 146 hasAttr(slog.MessageKey, "msg"), 147 missingKey("a"), 148 inGroup("G", hasAttr("a", "b")), 149 }, 150 }, 151 { 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") 155 }, 156 checks: []check{ 157 hasKey(slog.TimeKey), 158 hasKey(slog.LevelKey), 159 hasAttr(slog.MessageKey, "msg"), 160 hasAttr("a", "b"), 161 inGroup("G", hasAttr("c", "d")), 162 inGroup("G", inGroup("H", hasAttr("e", "f"))), 163 }, 164 }, 165 { 166 explanation: withSource("a Handler should call Resolve on attribute values"), 167 f: func(l *slog.Logger) { 168 l.Info("msg", "k", &replace{"replaced"}) 169 }, 170 checks: []check{hasAttr("k", "replaced")}, 171 }, 172 { 173 explanation: withSource("a Handler should call Resolve on attribute values in groups"), 174 f: func(l *slog.Logger) { 175 l.Info("msg", 176 slog.Group("G", 177 slog.String("a", "v1"), 178 slog.Any("b", &replace{"v2"}))) 179 }, 180 checks: []check{ 181 inGroup("G", hasAttr("a", "v1")), 182 inGroup("G", hasAttr("b", "v2")), 183 }, 184 }, 185 { 186 explanation: withSource("a Handler should call Resolve on attribute values from WithAttrs"), 187 f: func(l *slog.Logger) { 188 l = l.With("k", &replace{"replaced"}) 189 l.Info("msg") 190 }, 191 checks: []check{hasAttr("k", "replaced")}, 192 }, 193 { 194 explanation: withSource("a Handler should call Resolve on attribute values in groups from WithAttrs"), 195 f: func(l *slog.Logger) { 196 l = l.With(slog.Group("G", 197 slog.String("a", "v1"), 198 slog.Any("b", &replace{"v2"}))) 199 l.Info("msg") 200 }, 201 checks: []check{ 202 inGroup("G", hasAttr("a", "v1")), 203 inGroup("G", hasAttr("b", "v2")), 204 }, 205 }, 206 } 207 208 // Run the handler on the test cases. 209 for _, c := range cases { 210 ht := h 211 if c.mod != nil { 212 ht = &wrapper{h, c.mod} 213 } 214 l := slog.New(ht) 215 c.f(l) 216 } 217 218 // Collect and check the results. 219 var errs []error 220 res := results() 221 if g, w := len(res), len(cases); g != w { 222 return fmt.Errorf("got %d results, want %d", g, w) 223 } 224 for i, got := range results() { 225 c := cases[i] 226 for _, check := range c.checks { 227 if p := check(got); p != "" { 228 errs = append(errs, fmt.Errorf("%s: %s", p, c.explanation)) 229 } 230 } 231 } 232 return errors.Join(errs...) 233 } 234 235 type check func(map[string]any) string 236 237 func hasKey(key string) check { 238 return func(m map[string]any) string { 239 if _, ok := m[key]; !ok { 240 return fmt.Sprintf("missing key %q", key) 241 } 242 return "" 243 } 244 } 245 246 func missingKey(key string) check { 247 return func(m map[string]any) string { 248 if _, ok := m[key]; ok { 249 return fmt.Sprintf("unexpected key %q", key) 250 } 251 return "" 252 } 253 } 254 255 func hasAttr(key string, wantVal any) check { 256 return func(m map[string]any) string { 257 if s := hasKey(key)(m); s != "" { 258 return s 259 } 260 gotVal := m[key] 261 if !reflect.DeepEqual(gotVal, wantVal) { 262 return fmt.Sprintf("%q: got %#v, want %#v", key, gotVal, wantVal) 263 } 264 return "" 265 } 266 } 267 268 func inGroup(name string, c check) check { 269 return func(m map[string]any) string { 270 v, ok := m[name] 271 if !ok { 272 return fmt.Sprintf("missing group %q", name) 273 } 274 g, ok := v.(map[string]any) 275 if !ok { 276 return fmt.Sprintf("value for group %q is not map[string]any", name) 277 } 278 return c(g) 279 } 280 } 281 282 type wrapper struct { 283 slog.Handler 284 mod func(*slog.Record) 285 } 286 287 func (h *wrapper) Handle(ctx context.Context, r slog.Record) error { 288 h.mod(&r) 289 return h.Handler.Handle(ctx, r) 290 } 291 292 func withSource(s string) string { 293 _, file, line, ok := runtime.Caller(1) 294 if !ok { 295 panic("runtime.Caller failed") 296 } 297 return fmt.Sprintf("%s (%s:%d)", s, file, line) 298 } 299 300 type replace struct { 301 v any 302 } 303 304 func (r *replace) LogValue() slog.Value { return slog.AnyValue(r.v) } 305 306 func (r *replace) String() string { 307 return fmt.Sprintf("<replace(%v)>", r.v) 308 }