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 <gobot@golang.org> Reviewed-by: Ian Cottrell <iancottrell@google.com> Run-TryBot: Jonathan Amsterdam <jba@google.com>
1 parent 4228a15 commit b766721

File tree

5 files changed

+118
-81
lines changed

5 files changed

+118
-81
lines changed

go.mod

Lines changed: 2 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 56 additions & 3 deletions
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

Lines changed: 32 additions & 3 deletions
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

Lines changed: 25 additions & 75 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy