1// Copyright 2022 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
6
7import (
8	"bytes"
9	"context"
10	"errors"
11	"fmt"
12	"internal/testenv"
13	"io"
14	"strings"
15	"testing"
16	"time"
17)
18
19var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
20
21func TestTextHandler(t *testing.T) {
22	for _, test := range []struct {
23		name             string
24		attr             Attr
25		wantKey, wantVal string
26	}{
27		{
28			"unquoted",
29			Int("a", 1),
30			"a", "1",
31		},
32		{
33			"quoted",
34			String("x = y", `qu"o`),
35			`"x = y"`, `"qu\"o"`,
36		},
37		{
38			"String method",
39			Any("name", name{"Ren", "Hoek"}),
40			`name`, `"Hoek, Ren"`,
41		},
42		{
43			"struct",
44			Any("x", &struct{ A, b int }{A: 1, b: 2}),
45			`x`, `"&{A:1 b:2}"`,
46		},
47		{
48			"TextMarshaler",
49			Any("t", text{"abc"}),
50			`t`, `"text{\"abc\"}"`,
51		},
52		{
53			"TextMarshaler error",
54			Any("t", text{""}),
55			`t`, `"!ERROR:text: empty string"`,
56		},
57		{
58			"nil value",
59			Any("a", nil),
60			`a`, `<nil>`,
61		},
62	} {
63		t.Run(test.name, func(t *testing.T) {
64			for _, opts := range []struct {
65				name       string
66				opts       HandlerOptions
67				wantPrefix string
68				modKey     func(string) string
69			}{
70				{
71					"none",
72					HandlerOptions{},
73					`time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`,
74					func(s string) string { return s },
75				},
76				{
77					"replace",
78					HandlerOptions{ReplaceAttr: upperCaseKey},
79					`TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`,
80					strings.ToUpper,
81				},
82			} {
83				t.Run(opts.name, func(t *testing.T) {
84					var buf bytes.Buffer
85					h := NewTextHandler(&buf, &opts.opts)
86					r := NewRecord(testTime, LevelInfo, "a message", 0)
87					r.AddAttrs(test.attr)
88					if err := h.Handle(context.Background(), r); err != nil {
89						t.Fatal(err)
90					}
91					got := buf.String()
92					// Remove final newline.
93					got = got[:len(got)-1]
94					want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal
95					if got != want {
96						t.Errorf("\ngot  %s\nwant %s", got, want)
97					}
98				})
99			}
100		})
101	}
102}
103
104// for testing fmt.Sprint
105type name struct {
106	First, Last string
107}
108
109func (n name) String() string { return n.Last + ", " + n.First }
110
111// for testing TextMarshaler
112type text struct {
113	s string
114}
115
116func (t text) String() string { return t.s } // should be ignored
117
118func (t text) MarshalText() ([]byte, error) {
119	if t.s == "" {
120		return nil, errors.New("text: empty string")
121	}
122	return []byte(fmt.Sprintf("text{%q}", t.s)), nil
123}
124
125func TestTextHandlerPreformatted(t *testing.T) {
126	var buf bytes.Buffer
127	var h Handler = NewTextHandler(&buf, nil)
128	h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)})
129	// Also test omitting time.
130	r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0)
131	r.AddAttrs(Int("a", 1))
132	if err := h.Handle(context.Background(), r); err != nil {
133		t.Fatal(err)
134	}
135	got := strings.TrimSuffix(buf.String(), "\n")
136	want := `level=INFO msg=m dur=1m0s b=true a=1`
137	if got != want {
138		t.Errorf("got %s, want %s", got, want)
139	}
140}
141
142func TestTextHandlerAlloc(t *testing.T) {
143	testenv.SkipIfOptimizationOff(t)
144	r := NewRecord(time.Now(), LevelInfo, "msg", 0)
145	for i := 0; i < 10; i++ {
146		r.AddAttrs(Int("x = y", i))
147	}
148	var h Handler = NewTextHandler(io.Discard, nil)
149	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
150
151	h = h.WithGroup("s")
152	r.AddAttrs(Group("g", Int("a", 1)))
153	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
154}
155
156func TestNeedsQuoting(t *testing.T) {
157	for _, test := range []struct {
158		in   string
159		want bool
160	}{
161		{"", true},
162		{"ab", false},
163		{"a=b", true},
164		{`"ab"`, true},
165		{"\a\b", true},
166		{"a\tb", true},
167		{"µåπ", false},
168		{"a b", true},
169		{"badutf8\xF6", true},
170	} {
171		got := needsQuoting(test.in)
172		if got != test.want {
173			t.Errorf("%q: got %t, want %t", test.in, got, test.want)
174		}
175	}
176}
177