summaryrefslogtreecommitdiffstats
path: root/src/sync/oncefunc_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/sync/oncefunc_test.go')
-rw-r--r--src/sync/oncefunc_test.go265
1 files changed, 265 insertions, 0 deletions
diff --git a/src/sync/oncefunc_test.go b/src/sync/oncefunc_test.go
new file mode 100644
index 0000000..3c523a5
--- /dev/null
+++ b/src/sync/oncefunc_test.go
@@ -0,0 +1,265 @@
+// 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)
+ }
+ }
+ })
+}