Skip to content

Commit b766721

Browse files
committed
slog-handler-guide: add testing section
View at https://github.com/jba/markdown#testing. Change-Id: I0d8996395b03c3d1c7b7e50cce323a331fd614e1 Reviewed-on: https://go-review.googlesource.com/c/example/+/513137 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Ian Cottrell <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]>
1 parent 4228a15 commit b766721

File tree

5 files changed

+118
-81
lines changed

5 files changed

+118
-81
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ module golang.org/x/example
33
go 1.18
44

55
require golang.org/x/tools v0.0.0-20210112183307-1e6ecd4bf1b0
6+
7+
require gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
2222
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
2323
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
2424
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
25+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
26+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
27+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

slog-handler-guide/README.md

+56-3
Original file line numberDiff line numberDiff line change
@@ -706,15 +706,68 @@ Here there are no record attributes, so no group to open.
706706

707707
## Testing
708708

709+
The [`Handler` contract](https://pkg.go.dev/log/slog#Handler) specifies several
710+
constraints on handlers.
709711
To verify that your handler follows these rules and generally produces proper
710-
output, use the [testing/slogtest package](https://pkg.go.dev/log/slog).
712+
output, use the [testing/slogtest package](https://pkg.go.dev/testing/slogtest).
711713

712-
TODO(jba): show the test function.
714+
That package's `TestHandler` function takes an instance of your handler and
715+
a function that returns its output formatted as a slice of maps. Here is the test function
716+
for our example handler:
713717

714-
TODO(jba): reintroduce the material on Record.Clone that used to be here.
718+
```
719+
func TestSlogtest(t *testing.T) {
720+
var buf bytes.Buffer
721+
err := slogtest.TestHandler(New(&buf, nil), func() []map[string]any {
722+
return parseLogEntries(t, buf.Bytes())
723+
})
724+
if err != nil {
725+
t.Error(err)
726+
}
727+
}
728+
```
729+
730+
Calling `TestHandler` is easy. The hard part is parsing the output.
731+
`TestHandler` calls your handler multiple times, resulting in a sequence of log
732+
entries.
733+
It is your job to parse each entry into a `map[string]any`.
734+
A group in an entry should appear as a nested map.
735+
736+
If your handler outputs a standard format, you can use an existing parser.
737+
For example, if your handler outputs one JSON object per line, then you
738+
can split the output into lines and call `encoding/json.Unmarshal` on each.
739+
Parsers for other formats that can unmarshal into a map can be used out
740+
of the box.
741+
Our example output is enough like YAML so that we can use the `gopkg.in/yaml.v3`
742+
package to parse it:
743+
744+
```
745+
func parseLogEntries(t *testing.T, data []byte) []map[string]any {
746+
entries := bytes.Split(data, []byte("---\n"))
747+
entries = entries[:len(entries)-1] // last one is empty
748+
var ms []map[string]any
749+
for _, e := range entries {
750+
var m map[string]any
751+
if err := yaml.Unmarshal([]byte(e), &m); err != nil {
752+
t.Fatal(err)
753+
}
754+
ms = append(ms, m)
755+
}
756+
return ms
757+
}
758+
```
759+
760+
If you have to write your own parser, it can be far from perfect.
761+
The `slogtest` package uses only a handful of simple attributes.
762+
(It is testing handler conformance, not parsing.)
763+
Your parser can ignore edge cases like whitespace and newlines in keys and
764+
values. Before switching to a YAML parser, we wrote an adequate custom parser
765+
in 65 lines.
715766

716767
# General considerations
717768

769+
TODO(jba): reintroduce the material on Record.Clone that used to be here.
770+
718771
## Concurrency safety
719772

720773
A handler must work properly when a single `Logger` is shared among several

slog-handler-guide/guide.md

+32-3
Original file line numberDiff line numberDiff line change
@@ -449,15 +449,44 @@ Here there are no record attributes, so no group to open.
449449

450450
## Testing
451451

452+
The [`Handler` contract](https://pkg.go.dev/log/slog#Handler) specifies several
453+
constraints on handlers.
452454
To verify that your handler follows these rules and generally produces proper
453-
output, use the [testing/slogtest package](https://pkg.go.dev/log/slog).
455+
output, use the [testing/slogtest package](https://pkg.go.dev/testing/slogtest).
454456

455-
TODO(jba): show the test function.
457+
That package's `TestHandler` function takes an instance of your handler and
458+
a function that returns its output formatted as a slice of maps. Here is the test function
459+
for our example handler:
456460

457-
TODO(jba): reintroduce the material on Record.Clone that used to be here.
461+
%include indenthandler3/indent_handler_test.go TestSlogtest -
462+
463+
Calling `TestHandler` is easy. The hard part is parsing the output.
464+
`TestHandler` calls your handler multiple times, resulting in a sequence of log
465+
entries.
466+
It is your job to parse each entry into a `map[string]any`.
467+
A group in an entry should appear as a nested map.
468+
469+
If your handler outputs a standard format, you can use an existing parser.
470+
For example, if your handler outputs one JSON object per line, then you
471+
can split the output into lines and call `encoding/json.Unmarshal` on each.
472+
Parsers for other formats that can unmarshal into a map can be used out
473+
of the box.
474+
Our example output is enough like YAML so that we can use the `gopkg.in/yaml.v3`
475+
package to parse it:
476+
477+
%include indenthandler3/indent_handler_test.go parseLogEntries -
478+
479+
If you have to write your own parser, it can be far from perfect.
480+
The `slogtest` package uses only a handful of simple attributes.
481+
(It is testing handler conformance, not parsing.)
482+
Your parser can ignore edge cases like whitespace and newlines in keys and
483+
values. Before switching to a YAML parser, we wrote an adequate custom parser
484+
in 65 lines.
458485

459486
# General considerations
460487

488+
TODO(jba): reintroduce the material on Record.Clone that used to be here.
489+
461490
## Concurrency safety
462491

463492
A handler must work properly when a single `Logger` is shared among several

slog-handler-guide/indenthandler3/indent_handler_test.go

+25-75
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,29 @@
33
package indenthandler
44

55
import (
6-
"bufio"
76
"bytes"
8-
"fmt"
7+
"log/slog"
98
"reflect"
109
"regexp"
11-
"strconv"
12-
"strings"
1310
"testing"
1411
"testing/slogtest"
15-
"unicode"
1612

17-
"log/slog"
13+
"gopkg.in/yaml.v3"
1814
)
1915

16+
// !+TestSlogtest
2017
func TestSlogtest(t *testing.T) {
2118
var buf bytes.Buffer
2219
err := slogtest.TestHandler(New(&buf, nil), func() []map[string]any {
23-
return parseLogEntries(buf.String())
20+
return parseLogEntries(t, buf.Bytes())
2421
})
2522
if err != nil {
2623
t.Error(err)
2724
}
2825
}
2926

27+
// !-TestSlogtest
28+
3029
func Test(t *testing.T) {
3130
var buf bytes.Buffer
3231
l := slog.New(New(&buf, nil))
@@ -56,71 +55,22 @@ d: "NO"
5655
}
5756
}
5857

59-
func parseLogEntries(s string) []map[string]any {
58+
// !+parseLogEntries
59+
func parseLogEntries(t *testing.T, data []byte) []map[string]any {
60+
entries := bytes.Split(data, []byte("---\n"))
61+
entries = entries[:len(entries)-1] // last one is empty
6062
var ms []map[string]any
61-
scan := bufio.NewScanner(strings.NewReader(s))
62-
for scan.Scan() {
63-
m := parseGroup(scan)
63+
for _, e := range entries {
64+
var m map[string]any
65+
if err := yaml.Unmarshal([]byte(e), &m); err != nil {
66+
t.Fatal(err)
67+
}
6468
ms = append(ms, m)
6569
}
66-
if scan.Err() != nil {
67-
panic(scan.Err())
68-
}
6970
return ms
7071
}
7172

72-
func parseGroup(scan *bufio.Scanner) map[string]any {
73-
m := map[string]any{}
74-
groupIndent := -1
75-
for {
76-
line := scan.Text()
77-
if line == "---" { // end of entry
78-
break
79-
}
80-
k, v, found := strings.Cut(line, ":")
81-
if !found {
82-
panic(fmt.Sprintf("no ':' in line %q", line))
83-
}
84-
indent := strings.IndexFunc(k, func(r rune) bool {
85-
return !unicode.IsSpace(r)
86-
})
87-
if indent < 0 {
88-
panic("blank line")
89-
}
90-
if groupIndent < 0 {
91-
// First line in group; remember the indent.
92-
groupIndent = indent
93-
} else if indent < groupIndent {
94-
// End of group
95-
break
96-
} else if indent > groupIndent {
97-
panic(fmt.Sprintf("indent increased on line %q", line))
98-
}
99-
100-
key := strings.TrimSpace(k)
101-
if v == "" {
102-
// Just a key: start of a group.
103-
if !scan.Scan() {
104-
panic("empty group")
105-
}
106-
m[key] = parseGroup(scan)
107-
} else {
108-
v = strings.TrimSpace(v)
109-
if len(v) > 0 && v[0] == '"' {
110-
var err error
111-
v, err = strconv.Unquote(v)
112-
if err != nil {
113-
panic(err)
114-
}
115-
}
116-
m[key] = v
117-
if !scan.Scan() {
118-
break
119-
}
120-
}
121-
}
122-
return m
123-
}
73+
// !-parseLogEntries
12474

12575
func TestParseLogEntries(t *testing.T) {
12676
in := `
@@ -129,28 +79,28 @@ b: 2
12979
c: 3
13080
g:
13181
h: 4
132-
i: 5
82+
i: five
13383
d: 6
13484
---
13585
e: 7
13686
---
13787
`
13888
want := []map[string]any{
13989
{
140-
"a": "1",
141-
"b": "2",
142-
"c": "3",
90+
"a": 1,
91+
"b": 2,
92+
"c": 3,
14393
"g": map[string]any{
144-
"h": "4",
145-
"i": "5",
94+
"h": 4,
95+
"i": "five",
14696
},
147-
"d": "6",
97+
"d": 6,
14898
},
14999
{
150-
"e": "7",
100+
"e": 7,
151101
},
152102
}
153-
got := parseLogEntries(in[1:])
103+
got := parseLogEntries(t, []byte(in[1:]))
154104
if !reflect.DeepEqual(got, want) {
155105
t.Errorf("\ngot:\n%v\nwant:\n%v", got, want)
156106
}

0 commit comments

Comments
 (0)