Source file src/log/slog/value_test.go

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

View as plain text