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
5package slog_test
6
7import (
8	"bytes"
9	"encoding/json"
10	"fmt"
11	"io"
12	"log/slog"
13	"strings"
14	"testing"
15	"testing/slogtest"
16)
17
18func TestSlogtest(t *testing.T) {
19	for _, test := range []struct {
20		name  string
21		new   func(io.Writer) slog.Handler
22		parse func([]byte) (map[string]any, error)
23	}{
24		{"JSON", func(w io.Writer) slog.Handler { return slog.NewJSONHandler(w, nil) }, parseJSON},
25		{"Text", func(w io.Writer) slog.Handler { return slog.NewTextHandler(w, nil) }, parseText},
26	} {
27		t.Run(test.name, func(t *testing.T) {
28			var buf bytes.Buffer
29			h := test.new(&buf)
30			results := func() []map[string]any {
31				ms, err := parseLines(buf.Bytes(), test.parse)
32				if err != nil {
33					t.Fatal(err)
34				}
35				return ms
36			}
37			if err := slogtest.TestHandler(h, results); err != nil {
38				t.Fatal(err)
39			}
40		})
41	}
42}
43
44func parseLines(src []byte, parse func([]byte) (map[string]any, error)) ([]map[string]any, error) {
45	var records []map[string]any
46	for _, line := range bytes.Split(src, []byte{'\n'}) {
47		if len(line) == 0 {
48			continue
49		}
50		m, err := parse(line)
51		if err != nil {
52			return nil, fmt.Errorf("%s: %w", string(line), err)
53		}
54		records = append(records, m)
55	}
56	return records, nil
57}
58
59func parseJSON(bs []byte) (map[string]any, error) {
60	var m map[string]any
61	if err := json.Unmarshal(bs, &m); err != nil {
62		return nil, err
63	}
64	return m, nil
65}
66
67// parseText parses the output of a single call to TextHandler.Handle.
68// It can parse the output of the tests in this package,
69// but it doesn't handle quoted keys or values.
70// It doesn't need to handle all cases, because slogtest deliberately
71// uses simple inputs so handler writers can focus on testing
72// handler behavior, not parsing.
73func parseText(bs []byte) (map[string]any, error) {
74	top := map[string]any{}
75	s := string(bytes.TrimSpace(bs))
76	for len(s) > 0 {
77		kv, rest, _ := strings.Cut(s, " ") // assumes exactly one space between attrs
78		k, value, found := strings.Cut(kv, "=")
79		if !found {
80			return nil, fmt.Errorf("no '=' in %q", kv)
81		}
82		keys := strings.Split(k, ".")
83		// Populate a tree of maps for a dotted path such as "a.b.c=x".
84		m := top
85		for _, key := range keys[:len(keys)-1] {
86			x, ok := m[key]
87			var m2 map[string]any
88			if !ok {
89				m2 = map[string]any{}
90				m[key] = m2
91			} else {
92				m2, ok = x.(map[string]any)
93				if !ok {
94					return nil, fmt.Errorf("value for %q in composite key %q is not map[string]any", key, k)
95
96				}
97			}
98			m = m2
99		}
100		m[keys[len(keys)-1]] = value
101		s = rest
102	}
103	return top, nil
104}
105