blob: 3c523a5b62a96230aea4419d3e7719b8dd767634 [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 sync_test
import (
"bytes"
"runtime"
"runtime/debug"
"sync"
"testing"
)
// We assume that the Once.Do tests have already covered parallelism.
func TestOnceFunc(t *testing.T) {
calls := 0
f := sync.OnceFunc(func() { calls++ })
allocs := testing.AllocsPerRun(10, f)
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func TestOnceValue(t *testing.T) {
calls := 0
f := sync.OnceValue(func() int {
calls++
return calls
})
allocs := testing.AllocsPerRun(10, func() { f() })
value := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if value != 1 {
t.Errorf("want value==1, got %d", value)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func TestOnceValues(t *testing.T) {
calls := 0
f := sync.OnceValues(func() (int, int) {
calls++
return calls, calls + 1
})
allocs := testing.AllocsPerRun(10, func() { f() })
v1, v2 := f()
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
if v1 != 1 || v2 != 2 {
t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2)
}
if allocs != 0 {
t.Errorf("want 0 allocations per call, got %v", allocs)
}
}
func testOncePanicX(t *testing.T, calls *int, f func()) {
testOncePanicWith(t, calls, f, func(label string, p any) {
if p != "x" {
t.Fatalf("%s: want panic %v, got %v", label, "x", p)
}
})
}
func testOncePanicWith(t *testing.T, calls *int, f func(), check func(label string, p any)) {
// Check that the each call to f panics with the same value, but the
// underlying function is only called once.
for _, label := range []string{"first time", "second time"} {
var p any
panicked := true
func() {
defer func() {
p = recover()
}()
f()
panicked = false
}()
if !panicked {
t.Fatalf("%s: f did not panic", label)
}
check(label, p)
}
if *calls != 1 {
t.Errorf("want calls==1, got %d", *calls)
}
}
func TestOnceFuncPanic(t *testing.T) {
calls := 0
f := sync.OnceFunc(func() {
calls++
panic("x")
})
testOncePanicX(t, &calls, f)
}
func TestOnceValuePanic(t *testing.T) {
calls := 0
f := sync.OnceValue(func() int {
calls++
panic("x")
})
testOncePanicX(t, &calls, func() { f() })
}
func TestOnceValuesPanic(t *testing.T) {
calls := 0
f := sync.OnceValues(func() (int, int) {
calls++
panic("x")
})
testOncePanicX(t, &calls, func() { f() })
}
func TestOnceFuncPanicNil(t *testing.T) {
calls := 0
f := sync.OnceFunc(func() {
calls++
panic(nil)
})
testOncePanicWith(t, &calls, f, func(label string, p any) {
switch p.(type) {
case nil, *runtime.PanicNilError:
return
}
t.Fatalf("%s: want nil panic, got %v", label, p)
})
}
func TestOnceFuncGoexit(t *testing.T) {
// If f calls Goexit, the results are unspecified. But check that f doesn't
// get called twice.
calls := 0
f := sync.OnceFunc(func() {
calls++
runtime.Goexit()
})
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
defer func() { recover() }()
f()
}()
wg.Wait()
}
if calls != 1 {
t.Errorf("want calls==1, got %d", calls)
}
}
func TestOnceFuncPanicTraceback(t *testing.T) {
// Test that on the first invocation of a OnceFunc, the stack trace goes all
// the way to the origin of the panic.
f := sync.OnceFunc(onceFuncPanic)
defer func() {
if p := recover(); p != "x" {
t.Fatalf("want panic %v, got %v", "x", p)
}
stack := debug.Stack()
want := "sync_test.onceFuncPanic"
if !bytes.Contains(stack, []byte(want)) {
t.Fatalf("want stack containing %v, got:\n%s", want, string(stack))
}
}()
f()
}
func onceFuncPanic() {
panic("x")
}
var (
onceFunc = sync.OnceFunc(func() {})
onceFuncOnce sync.Once
)
func doOnceFunc() {
onceFuncOnce.Do(func() {})
}
func BenchmarkOnceFunc(b *testing.B) {
b.Run("v=Once", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// The baseline is direct use of sync.Once.
doOnceFunc()
}
})
b.Run("v=Global", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// As of 3/2023, the compiler doesn't recognize that onceFunc is
// never mutated and is a closure that could be inlined.
// Too bad, because this is how OnceFunc will usually be used.
onceFunc()
}
})
b.Run("v=Local", func(b *testing.B) {
b.ReportAllocs()
// As of 3/2023, the compiler *does* recognize this local binding as an
// inlinable closure. This is the best case for OnceFunc, but probably
// not typical usage.
f := sync.OnceFunc(func() {})
for i := 0; i < b.N; i++ {
f()
}
})
}
var (
onceValue = sync.OnceValue(func() int { return 42 })
onceValueOnce sync.Once
onceValueValue int
)
func doOnceValue() int {
onceValueOnce.Do(func() {
onceValueValue = 42
})
return onceValueValue
}
func BenchmarkOnceValue(b *testing.B) {
// See BenchmarkOnceFunc
b.Run("v=Once", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if want, got := 42, doOnceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
b.Run("v=Global", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if want, got := 42, onceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
b.Run("v=Local", func(b *testing.B) {
b.ReportAllocs()
onceValue := sync.OnceValue(func() int { return 42 })
for i := 0; i < b.N; i++ {
if want, got := 42, onceValue(); want != got {
b.Fatalf("want %d, got %d", want, got)
}
}
})
}