blob: 89ab6fcd6f1f7c9cd2112b86c23f6775bdac65a6 [file] [log] [blame]
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package slog
import (
"fmt"
"testing"
"time"
"unsafe"
)
func TestEqual(t *testing.T) {
var x, y int
vals := []Attr{
{},
Int64("key", 1),
Int64("key", 2),
Float64("key", 3.5),
Float64("key", 3.7),
Bool("key", true),
Bool("key", false),
Any("key", &x),
Any("key", &y),
}
for i, v1 := range vals {
for j, v2 := range vals {
got := v1.Equal(v2)
want := i == j
if got != want {
t.Errorf("%v.Equal(%v): got %t, want %t", v1, v2, got, want)
}
}
}
}
func TestNilAttr(t *testing.T) {
n := Any("k", nil)
if g := n.Value(); g != nil {
t.Errorf("got %#v, want nil", g)
}
}
func panics(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f()
return false
}
func TestString(t *testing.T) {
for _, test := range []struct {
v Attr
want string
}{
{Int64("key", -3), "-3"},
{Float64("key", .15), "0.15"},
{Bool("key", true), "true"},
{String("key", "foo"), "foo"},
{Any("key", time.Duration(3*time.Second)), "3s"},
} {
if got := test.v.String(); got != test.want {
t.Errorf("%#v: got %q, want %q", test.v, got, test.want)
}
}
}
func TestAttrNoAlloc(t *testing.T) {
// Assign values just to make sure the compiler doesn't optimize away the statements.
var (
i int64
u uint64
f float64
b bool
s string
x any
p = &i
d time.Duration
)
a := int(testing.AllocsPerRun(5, func() {
i = Int64("key", 1).Int64()
u = Uint64("key", 1).Uint64()
f = Float64("key", 1).Float64()
b = Bool("key", true).Bool()
s = String("key", "foo").String()
d = Duration("key", d).Duration()
x = Any("key", p).Value()
}))
if a != 0 {
t.Errorf("got %d allocs, want zero", a)
}
_ = u
_ = f
_ = b
_ = s
_ = x
}
func TestAnyLevelAlloc(t *testing.T) {
// Because typical Levels are small integers,
// they are zero-alloc.
var a Attr
x := DebugLevel + 100
wantAllocs(t, 0, func() { a = Any("k", x) })
_ = a
}
func TestAnyLevel(t *testing.T) {
x := DebugLevel + 100
a := Any("k", x)
v := a.Value()
if _, ok := v.(Level); !ok {
t.Errorf("wanted Level, got %T", v)
}
}
//////////////// Benchmark for accessing Attr values
// The "As" form is the slowest.
// The switch-panic and visitor times are almost the same.
// BenchmarkDispatch/switch-checked-8 8669427 137.7 ns/op
// BenchmarkDispatch/As-8 8212087 145.3 ns/op
// BenchmarkDispatch/Visit-8 8926146 135.3 ns/op
func BenchmarkDispatch(b *testing.B) {
kvs := []Attr{
Int64("i", 32768),
Uint64("u", 0xfacecafe),
String("s", "anything"),
Bool("b", true),
Float64("f", 1.2345),
Duration("d", time.Second),
Any("a", b),
}
var (
ii int64
s string
bb bool
u uint64
d time.Duration
f float64
a any
)
b.Run("switch-checked", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, kv := range kvs {
switch kv.Kind() {
case StringKind:
s = kv.String()
case Int64Kind:
ii = kv.Int64()
case Uint64Kind:
u = kv.Uint64()
case Float64Kind:
f = kv.Float64()
case BoolKind:
bb = kv.Bool()
case DurationKind:
d = kv.Duration()
case AnyKind:
a = kv.Value()
default:
panic("bad kind")
}
}
}
_ = ii
_ = s
_ = bb
_ = u
_ = d
_ = f
_ = a
})
b.Run("As", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, kv := range kvs {
if v, ok := kv.AsString(); ok {
s = v
} else if v, ok := kv.AsInt64(); ok {
ii = v
} else if v, ok := kv.AsUint64(); ok {
u = v
} else if v, ok := kv.AsFloat64(); ok {
f = v
} else if v, ok := kv.AsBool(); ok {
bb = v
} else if v, ok := kv.AsDuration(); ok {
d = v
} else if v, ok := kv.AsAny(); ok {
a = v
} else {
panic("bad kind")
}
}
}
_ = ii
_ = s
_ = bb
_ = u
_ = d
_ = f
_ = a
})
b.Run("Visit", func(b *testing.B) {
v := &setVisitor{}
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, kv := range kvs {
kv.Visit(v)
}
}
})
}
type setVisitor struct {
i int64
s string
b bool
u uint64
d time.Duration
f float64
a any
}
func (v *setVisitor) String(s string) { v.s = s }
func (v *setVisitor) Int64(i int64) { v.i = i }
func (v *setVisitor) Uint64(x uint64) { v.u = x }
func (v *setVisitor) Float64(x float64) { v.f = x }
func (v *setVisitor) Bool(x bool) { v.b = x }
func (v *setVisitor) Duration(x time.Duration) { v.d = x }
func (v *setVisitor) Any(x any) { v.a = x }
// When dispatching on all types, the "As" functions are slightly slower
// than switching on the kind and then calling a function that checks
// the kind again. See BenchmarkDispatch above.
func (a Attr) AsString() (string, bool) {
if a.Kind() == StringKind {
return a.str(), true
}
return "", false
}
func (a Attr) AsInt64() (int64, bool) {
if a.Kind() == Int64Kind {
return int64(a.num), true
}
return 0, false
}
func (a Attr) AsUint64() (uint64, bool) {
if a.Kind() == Uint64Kind {
return a.num, true
}
return 0, false
}
func (a Attr) AsFloat64() (float64, bool) {
if a.Kind() == Float64Kind {
return a.float(), true
}
return 0, false
}
func (a Attr) AsBool() (bool, bool) {
if a.Kind() == BoolKind {
return a.bool(), true
}
return false, false
}
func (a Attr) AsDuration() (time.Duration, bool) {
if a.Kind() == DurationKind {
return a.duration(), true
}
return 0, false
}
func (a Attr) AsAny() (any, bool) {
if a.Kind() == AnyKind {
return a.any, true
}
return nil, false
}
// Problem: adding a type means adding a method, which is a breaking change.
// Using an unexported method to force embedding will make programs compile,
// But they will panic at runtime when we call the new method.
type Visitor interface {
String(string)
Int64(int64)
Uint64(uint64)
Float64(float64)
Bool(bool)
Duration(time.Duration)
Any(any)
}
func (a Attr) Visit(v Visitor) {
switch a.Kind() {
case StringKind:
v.String(a.str())
case Int64Kind:
v.Int64(int64(a.num))
case Uint64Kind:
v.Uint64(a.num)
case BoolKind:
v.Bool(a.bool())
case Float64Kind:
v.Float64(a.float())
case DurationKind:
v.Duration(a.duration())
case AnyKind:
v.Any(a.any)
default:
panic("bad kind")
}
}
// A Attr with "unsafe" strings is significantly faster:
// safe: 1785 ns/op, 0 allocs
// unsafe: 690 ns/op, 0 allocs
// Run this with and without -tags unsafe_kvs to compare.
func BenchmarkUnsafeStrings(b *testing.B) {
b.ReportAllocs()
dst := make([]Attr, 100)
src := make([]Attr, len(dst))
b.Logf("Attr size = %d", unsafe.Sizeof(Attr{}))
for i := range src {
src[i] = String("k", fmt.Sprintf("string#%d", i))
}
b.ResetTimer()
var d string
for i := 0; i < b.N; i++ {
copy(dst, src)
for _, a := range dst {
d = a.String()
}
}
_ = d
}