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	"fmt"
9	"reflect"
10	"strings"
11	"testing"
12	"time"
13	"unsafe"
14)
15
16func TestKindString(t *testing.T) {
17	if got, want := KindGroup.String(), "Group"; got != want {
18		t.Errorf("got %q, want %q", got, want)
19	}
20}
21
22func TestValueEqual(t *testing.T) {
23	var x, y int
24	vals := []Value{
25		{},
26		Int64Value(1),
27		Int64Value(2),
28		Float64Value(3.5),
29		Float64Value(3.7),
30		BoolValue(true),
31		BoolValue(false),
32		TimeValue(testTime),
33		TimeValue(time.Time{}),
34		TimeValue(time.Date(2001, 1, 2, 3, 4, 5, 0, time.UTC)),
35		TimeValue(time.Date(2300, 1, 1, 0, 0, 0, 0, time.UTC)),            // overflows nanoseconds
36		TimeValue(time.Date(1715, 6, 13, 0, 25, 26, 290448384, time.UTC)), // overflowed value
37		AnyValue(&x),
38		AnyValue(&y),
39		GroupValue(Bool("b", true), Int("i", 3)),
40		GroupValue(Bool("b", true), Int("i", 4)),
41		GroupValue(Bool("b", true), Int("j", 4)),
42		DurationValue(3 * time.Second),
43		DurationValue(2 * time.Second),
44		StringValue("foo"),
45		StringValue("fuu"),
46	}
47	for i, v1 := range vals {
48		for j, v2 := range vals {
49			got := v1.Equal(v2)
50			want := i == j
51			if got != want {
52				t.Errorf("%v.Equal(%v): got %t, want %t", v1, v2, got, want)
53			}
54		}
55	}
56}
57
58func panics(f func()) (b bool) {
59	defer func() {
60		if x := recover(); x != nil {
61			b = true
62		}
63	}()
64	f()
65	return false
66}
67
68func TestValueString(t *testing.T) {
69	for _, test := range []struct {
70		v    Value
71		want string
72	}{
73		{Int64Value(-3), "-3"},
74		{Uint64Value(1), "1"},
75		{Float64Value(.15), "0.15"},
76		{BoolValue(true), "true"},
77		{StringValue("foo"), "foo"},
78		{TimeValue(testTime), "2000-01-02 03:04:05 +0000 UTC"},
79		{AnyValue(time.Duration(3 * time.Second)), "3s"},
80		{GroupValue(Int("a", 1), Bool("b", true)), "[a=1 b=true]"},
81	} {
82		if got := test.v.String(); got != test.want {
83			t.Errorf("%#v:\ngot  %q\nwant %q", test.v, got, test.want)
84		}
85	}
86}
87
88func TestValueNoAlloc(t *testing.T) {
89	// Assign values just to make sure the compiler doesn't optimize away the statements.
90	var (
91		i  int64
92		u  uint64
93		f  float64
94		b  bool
95		s  string
96		x  any
97		p  = &i
98		d  time.Duration
99		tm time.Time
100	)
101	a := int(testing.AllocsPerRun(5, func() {
102		i = Int64Value(1).Int64()
103		u = Uint64Value(1).Uint64()
104		f = Float64Value(1).Float64()
105		b = BoolValue(true).Bool()
106		s = StringValue("foo").String()
107		d = DurationValue(d).Duration()
108		tm = TimeValue(testTime).Time()
109		x = AnyValue(p).Any()
110	}))
111	if a != 0 {
112		t.Errorf("got %d allocs, want zero", a)
113	}
114	_ = u
115	_ = f
116	_ = b
117	_ = s
118	_ = x
119	_ = tm
120}
121
122func TestAnyLevelAlloc(t *testing.T) {
123	// Because typical Levels are small integers,
124	// they are zero-alloc.
125	var a Value
126	x := LevelDebug + 100
127	wantAllocs(t, 0, func() { a = AnyValue(x) })
128	_ = a
129}
130
131func TestAnyValue(t *testing.T) {
132	for _, test := range []struct {
133		in   any
134		want Value
135	}{
136		{1, IntValue(1)},
137		{1.5, Float64Value(1.5)},
138		{float32(2.5), Float64Value(2.5)},
139		{"s", StringValue("s")},
140		{true, BoolValue(true)},
141		{testTime, TimeValue(testTime)},
142		{time.Hour, DurationValue(time.Hour)},
143		{[]Attr{Int("i", 3)}, GroupValue(Int("i", 3))},
144		{IntValue(4), IntValue(4)},
145		{uint(2), Uint64Value(2)},
146		{uint8(3), Uint64Value(3)},
147		{uint16(4), Uint64Value(4)},
148		{uint32(5), Uint64Value(5)},
149		{uint64(6), Uint64Value(6)},
150		{uintptr(7), Uint64Value(7)},
151		{int8(8), Int64Value(8)},
152		{int16(9), Int64Value(9)},
153		{int32(10), Int64Value(10)},
154		{int64(11), Int64Value(11)},
155	} {
156		got := AnyValue(test.in)
157		if !got.Equal(test.want) {
158			t.Errorf("%v (%[1]T): got %v (kind %s), want %v (kind %s)",
159				test.in, got, got.Kind(), test.want, test.want.Kind())
160		}
161	}
162}
163
164func TestValueAny(t *testing.T) {
165	for _, want := range []any{
166		nil,
167		LevelDebug + 100,
168		time.UTC, // time.Locations treated specially...
169		KindBool, // ...as are Kinds
170		[]Attr{Int("a", 1)},
171		int64(2),
172		uint64(3),
173		true,
174		time.Minute,
175		time.Time{},
176		3.14,
177		"foo",
178	} {
179		v := AnyValue(want)
180		got := v.Any()
181		if !reflect.DeepEqual(got, want) {
182			t.Errorf("got %v, want %v", got, want)
183		}
184	}
185}
186
187func TestLogValue(t *testing.T) {
188	want := "replaced"
189	r := &replace{StringValue(want)}
190	v := AnyValue(r)
191	if g, w := v.Kind(), KindLogValuer; g != w {
192		t.Errorf("got %s, want %s", g, w)
193	}
194	got := v.LogValuer().LogValue().Any()
195	if got != want {
196		t.Errorf("got %#v, want %#v", got, want)
197	}
198
199	// Test Resolve.
200	got = v.Resolve().Any()
201	if got != want {
202		t.Errorf("got %#v, want %#v", got, want)
203	}
204
205	// Test Resolve max iteration.
206	r.v = AnyValue(r) // create a cycle
207	got = AnyValue(r).Resolve().Any()
208	if _, ok := got.(error); !ok {
209		t.Errorf("expected error, got %T", got)
210	}
211
212	// Groups are not recursively resolved.
213	c := Any("c", &replace{StringValue("d")})
214	v = AnyValue(&replace{GroupValue(Int("a", 1), Group("b", c))})
215	got2 := v.Resolve().Any().([]Attr)
216	want2 := []Attr{Int("a", 1), Group("b", c)}
217	if !attrsEqual(got2, want2) {
218		t.Errorf("got %v, want %v", got2, want2)
219	}
220
221	// Verify that panics in Resolve are caught and turn into errors.
222	v = AnyValue(panickingLogValue{})
223	got = v.Resolve().Any()
224	gotErr, ok := got.(error)
225	if !ok {
226		t.Errorf("expected error, got %T", got)
227	}
228	// The error should provide some context information.
229	// We'll just check that this function name appears in it.
230	if got, want := gotErr.Error(), "TestLogValue"; !strings.Contains(got, want) {
231		t.Errorf("got %q, want substring %q", got, want)
232	}
233}
234
235func TestValueTime(t *testing.T) {
236	// Validate that all representations of times work correctly.
237	for _, tm := range []time.Time{
238		time.Time{},
239		time.Unix(0, 1e15), // UnixNanos is defined
240		time.Date(2300, 1, 1, 0, 0, 0, 0, time.UTC), // overflows UnixNanos
241	} {
242		got := TimeValue(tm).Time()
243		if !got.Equal(tm) {
244			t.Errorf("got %s (%#[1]v), want %s (%#[2]v)", got, tm)
245		}
246		if g, w := got.Location(), tm.Location(); g != w {
247			t.Errorf("%s: location: got %v, want %v", tm, g, w)
248		}
249	}
250}
251
252func TestEmptyGroup(t *testing.T) {
253	g := GroupValue(
254		Int("a", 1),
255		Group("g1", Group("g2")),
256		Group("g3", Group("g4", Int("b", 2))))
257	got := g.Group()
258	want := []Attr{Int("a", 1), Group("g3", Group("g4", Int("b", 2)))}
259	if !attrsEqual(got, want) {
260		t.Errorf("\ngot  %v\nwant %v", got, want)
261	}
262}
263
264type replace struct {
265	v Value
266}
267
268func (r *replace) LogValue() Value { return r.v }
269
270type panickingLogValue struct{}
271
272func (panickingLogValue) LogValue() Value { panic("bad") }
273
274// A Value with "unsafe" strings is significantly faster:
275// safe:  1785 ns/op, 0 allocs
276// unsafe: 690 ns/op, 0 allocs
277
278// Run this with and without -tags unsafe_kvs to compare.
279func BenchmarkUnsafeStrings(b *testing.B) {
280	b.ReportAllocs()
281	dst := make([]Value, 100)
282	src := make([]Value, len(dst))
283	b.Logf("Value size = %d", unsafe.Sizeof(Value{}))
284	for i := range src {
285		src[i] = StringValue(fmt.Sprintf("string#%d", i))
286	}
287	b.ResetTimer()
288	var d string
289	for i := 0; i < b.N; i++ {
290		copy(dst, src)
291		for _, a := range dst {
292			d = a.String()
293		}
294	}
295	_ = d
296}
297