// 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 ( "slices" "strconv" "strings" "testing" "time" ) func TestRecordAttrs(t *testing.T) { as := []Attr{Int("k1", 1), String("k2", "foo"), Int("k3", 3), Int64("k4", -1), Float64("f", 3.1), Uint64("u", 999)} r := newRecordWithAttrs(as) if g, w := r.NumAttrs(), len(as); g != w { t.Errorf("NumAttrs: got %d, want %d", g, w) } if got := attrsSlice(r); !attrsEqual(got, as) { t.Errorf("got %v, want %v", got, as) } // Early return. // Hit both loops in Record.Attrs: front and back. for _, stop := range []int{2, 6} { var got []Attr r.Attrs(func(a Attr) bool { got = append(got, a) return len(got) < stop }) want := as[:stop] if !attrsEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } } func TestRecordSource(t *testing.T) { // Zero call depth => empty *Source. for _, test := range []struct { depth int wantFunction string wantFile string wantLinePositive bool }{ {0, "", "", false}, {-16, "", "", false}, {1, "log/slog.TestRecordSource", "record_test.go", true}, // 1: caller of NewRecord {2, "testing.tRunner", "testing.go", true}, } { var pc uintptr if test.depth > 0 { pc = callerPC(test.depth + 1) } r := NewRecord(time.Time{}, 0, "", pc) got := r.source() if i := strings.LastIndexByte(got.File, '/'); i >= 0 { got.File = got.File[i+1:] } if got.Function != test.wantFunction || got.File != test.wantFile || (got.Line > 0) != test.wantLinePositive { t.Errorf("depth %d: got (%q, %q, %d), want (%q, %q, %t)", test.depth, got.Function, got.File, got.Line, test.wantFunction, test.wantFile, test.wantLinePositive) } } } func TestAliasingAndClone(t *testing.T) { intAttrs := func(from, to int) []Attr { var as []Attr for i := from; i < to; i++ { as = append(as, Int("k", i)) } return as } check := func(r Record, want []Attr) { t.Helper() got := attrsSlice(r) if !attrsEqual(got, want) { t.Errorf("got %v, want %v", got, want) } } // Create a record whose Attrs overflow the inline array, // creating a slice in r.back. r1 := NewRecord(time.Time{}, 0, "", 0) r1.AddAttrs(intAttrs(0, nAttrsInline+1)...) // Ensure that r1.back's capacity exceeds its length. b := make([]Attr, len(r1.back), len(r1.back)+1) copy(b, r1.back) r1.back = b // Make a copy that shares state. r2 := r1 // Adding to both should insert a special Attr in the second. r1AttrsBefore := attrsSlice(r1) r1.AddAttrs(Int("p", 0)) r2.AddAttrs(Int("p", 1)) check(r1, append(slices.Clip(r1AttrsBefore), Int("p", 0))) r1Attrs := attrsSlice(r1) check(r2, append(slices.Clip(r1AttrsBefore), String("!BUG", "AddAttrs unsafely called on copy of Record made without using Record.Clone"), Int("p", 1))) // Adding to a clone is fine. r2 = r1.Clone() check(r2, r1Attrs) r2.AddAttrs(Int("p", 2)) check(r1, r1Attrs) // r1 is unchanged check(r2, append(slices.Clip(r1Attrs), Int("p", 2))) } func newRecordWithAttrs(as []Attr) Record { r := NewRecord(time.Now(), LevelInfo, "", 0) r.AddAttrs(as...) return r } func attrsSlice(r Record) []Attr { s := make([]Attr, 0, r.NumAttrs()) r.Attrs(func(a Attr) bool { s = append(s, a); return true }) return s } func attrsEqual(as1, as2 []Attr) bool { return slices.EqualFunc(as1, as2, Attr.Equal) } // Currently, pc(2) takes over 400ns, which is too expensive // to call it for every log message. func BenchmarkPC(b *testing.B) { for depth := 0; depth < 5; depth++ { b.Run(strconv.Itoa(depth), func(b *testing.B) { b.ReportAllocs() var x uintptr for i := 0; i < b.N; i++ { x = callerPC(depth) } _ = x }) } } func BenchmarkRecord(b *testing.B) { const nAttrs = nAttrsInline * 10 var a Attr for i := 0; i < b.N; i++ { r := NewRecord(time.Time{}, LevelInfo, "", 0) for j := 0; j < nAttrs; j++ { r.AddAttrs(Int("k", j)) } r.Attrs(func(b Attr) bool { a = b; return true }) } _ = a }