blob: 73a0a1c453e3d5053eb2a633145030ba86074995 [file] [log] [blame] [edit]
// Copyright 2024 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 synctest_test
import (
"fmt"
"internal/synctest"
"internal/testenv"
"iter"
"os"
"reflect"
"runtime"
"slices"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"weak"
)
func TestNow(t *testing.T) {
start := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC).In(time.Local)
synctest.Run(func() {
// Time starts at 2000-1-1 00:00:00.
if got, want := time.Now(), start; !got.Equal(want) {
t.Errorf("at start: time.Now = %v, want %v", got, want)
}
go func() {
// New goroutines see the same fake clock.
if got, want := time.Now(), start; !got.Equal(want) {
t.Errorf("time.Now = %v, want %v", got, want)
}
}()
// Time advances after a sleep.
time.Sleep(1 * time.Second)
if got, want := time.Now(), start.Add(1*time.Second); !got.Equal(want) {
t.Errorf("after sleep: time.Now = %v, want %v", got, want)
}
})
}
// TestMonotonicClock exercises comparing times from within a bubble
// with ones from outside the bubble.
func TestMonotonicClock(t *testing.T) {
start := time.Now()
synctest.Run(func() {
time.Sleep(time.Until(start.Round(0)))
if got, want := time.Now().In(time.UTC), start.In(time.UTC); !got.Equal(want) {
t.Fatalf("time.Now() = %v, want %v", got, want)
}
wait := 1 * time.Second
time.Sleep(wait)
if got := time.Since(start); got != wait {
t.Fatalf("time.Since(start) = %v, want %v", got, wait)
}
if got := time.Now().Sub(start); got != wait {
t.Fatalf("time.Now().Sub(start) = %v, want %v", got, wait)
}
})
}
func TestRunEmpty(t *testing.T) {
synctest.Run(func() {
})
}
func TestSimpleWait(t *testing.T) {
synctest.Run(func() {
synctest.Wait()
})
}
func TestGoroutineWait(t *testing.T) {
synctest.Run(func() {
go func() {}()
synctest.Wait()
})
}
// TestWait starts a collection of goroutines.
// It checks that synctest.Wait waits for all goroutines to exit before returning.
func TestWait(t *testing.T) {
synctest.Run(func() {
done := false
ch := make(chan int)
var f func()
f = func() {
count := <-ch
if count == 0 {
done = true
} else {
go f()
ch <- count - 1
}
}
go f()
ch <- 100
synctest.Wait()
if !done {
t.Fatalf("done = false, want true")
}
})
}
func TestMallocs(t *testing.T) {
for i := 0; i < 100; i++ {
synctest.Run(func() {
done := false
ch := make(chan []byte)
var f func()
f = func() {
b := <-ch
if len(b) == 0 {
done = true
} else {
go f()
ch <- make([]byte, len(b)-1)
}
}
go f()
ch <- make([]byte, 100)
synctest.Wait()
if !done {
t.Fatalf("done = false, want true")
}
})
}
}
func TestTimerReadBeforeDeadline(t *testing.T) {
synctest.Run(func() {
start := time.Now()
tm := time.NewTimer(5 * time.Second)
<-tm.C
if got, want := time.Since(start), 5*time.Second; got != want {
t.Errorf("after sleep: time.Since(start) = %v, want %v", got, want)
}
})
}
func TestTimerReadAfterDeadline(t *testing.T) {
synctest.Run(func() {
delay := 1 * time.Second
want := time.Now().Add(delay)
tm := time.NewTimer(delay)
time.Sleep(2 * delay)
got := <-tm.C
if got != want {
t.Errorf("<-tm.C = %v, want %v", got, want)
}
})
}
func TestTimerReset(t *testing.T) {
synctest.Run(func() {
start := time.Now()
tm := time.NewTimer(1 * time.Second)
if got, want := <-tm.C, start.Add(1*time.Second); got != want {
t.Errorf("first sleep: <-tm.C = %v, want %v", got, want)
}
tm.Reset(2 * time.Second)
if got, want := <-tm.C, start.Add((1+2)*time.Second); got != want {
t.Errorf("second sleep: <-tm.C = %v, want %v", got, want)
}
tm.Reset(3 * time.Second)
time.Sleep(1 * time.Second)
tm.Reset(3 * time.Second)
if got, want := <-tm.C, start.Add((1+2+4)*time.Second); got != want {
t.Errorf("third sleep: <-tm.C = %v, want %v", got, want)
}
})
}
func TestTimeAfter(t *testing.T) {
synctest.Run(func() {
i := 0
time.AfterFunc(1*time.Second, func() {
// Ensure synctest group membership propagates through the AfterFunc.
i++ // 1
go func() {
time.Sleep(1 * time.Second)
i++ // 2
}()
})
time.Sleep(3 * time.Second)
synctest.Wait()
if got, want := i, 2; got != want {
t.Errorf("after sleep and wait: i = %v, want %v", got, want)
}
})
}
func TestTimerAfterBubbleExit(t *testing.T) {
run := false
synctest.Run(func() {
time.AfterFunc(1*time.Second, func() {
run = true
})
})
if run {
t.Errorf("timer ran before bubble exit")
}
}
func TestTimerFromOutsideBubble(t *testing.T) {
tm := time.NewTimer(10 * time.Millisecond)
synctest.Run(func() {
<-tm.C
})
if tm.Stop() {
t.Errorf("synctest.Run unexpectedly returned before timer fired")
}
}
// TestTimerNondeterminism verifies that timers firing at the same instant
// don't always fire in exactly the same order.
func TestTimerNondeterminism(t *testing.T) {
synctest.Run(func() {
const iterations = 1000
var seen1, seen2 bool
for range iterations {
tm1 := time.NewTimer(1)
tm2 := time.NewTimer(1)
select {
case <-tm1.C:
seen1 = true
case <-tm2.C:
seen2 = true
}
if seen1 && seen2 {
return
}
synctest.Wait()
}
t.Errorf("after %v iterations, seen timer1:%v, timer2:%v; want both", iterations, seen1, seen2)
})
}
// TestSleepNondeterminism verifies that goroutines sleeping to the same instant
// don't always schedule in exactly the same order.
func TestSleepNondeterminism(t *testing.T) {
synctest.Run(func() {
const iterations = 1000
var seen1, seen2 bool
for range iterations {
var first atomic.Int32
go func() {
time.Sleep(1)
first.CompareAndSwap(0, 1)
}()
go func() {
time.Sleep(1)
first.CompareAndSwap(0, 2)
}()
time.Sleep(1)
synctest.Wait()
switch v := first.Load(); v {
case 1:
seen1 = true
case 2:
seen2 = true
default:
t.Fatalf("first = %v, want 1 or 2", v)
}
if seen1 && seen2 {
return
}
synctest.Wait()
}
t.Errorf("after %v iterations, seen goroutine 1:%v, 2:%v; want both", iterations, seen1, seen2)
})
}
// TestTimerRunsImmediately verifies that a 0-duration timer sends on its channel
// without waiting for the bubble to block.
func TestTimerRunsImmediately(t *testing.T) {
synctest.Run(func() {
start := time.Now()
tm := time.NewTimer(0)
select {
case got := <-tm.C:
if !got.Equal(start) {
t.Errorf("<-tm.C = %v, want %v", got, start)
}
default:
t.Errorf("0-duration timer channel is not readable; want it to be")
}
})
}
// TestTimerRunsLater verifies that reading from a timer's channel receives the
// timer fired, even when that time is in reading from a timer's channel receives the
// time the timer fired, even when that time is in the past.
func TestTimerRanInPast(t *testing.T) {
synctest.Run(func() {
delay := 1 * time.Second
want := time.Now().Add(delay)
tm := time.NewTimer(delay)
time.Sleep(2 * delay)
select {
case got := <-tm.C:
if !got.Equal(want) {
t.Errorf("<-tm.C = %v, want %v", got, want)
}
default:
t.Errorf("0-duration timer channel is not readable; want it to be")
}
})
}
// TestAfterFuncRunsImmediately verifies that a 0-duration AfterFunc is scheduled
// without waiting for the bubble to block.
func TestAfterFuncRunsImmediately(t *testing.T) {
synctest.Run(func() {
var b atomic.Bool
time.AfterFunc(0, func() {
b.Store(true)
})
for !b.Load() {
runtime.Gosched()
}
})
}
func TestChannelFromOutsideBubble(t *testing.T) {
choutside := make(chan struct{})
for _, test := range []struct {
desc string
outside func(ch chan int)
inside func(ch chan int)
}{{
desc: "read closed",
outside: func(ch chan int) { close(ch) },
inside: func(ch chan int) { <-ch },
}, {
desc: "read value",
outside: func(ch chan int) { ch <- 0 },
inside: func(ch chan int) { <-ch },
}, {
desc: "write value",
outside: func(ch chan int) { <-ch },
inside: func(ch chan int) { ch <- 0 },
}, {
desc: "select outside only",
outside: func(ch chan int) { close(ch) },
inside: func(ch chan int) {
select {
case <-ch:
case <-choutside:
}
},
}, {
desc: "select mixed",
outside: func(ch chan int) { close(ch) },
inside: func(ch chan int) {
ch2 := make(chan struct{})
select {
case <-ch:
case <-ch2:
}
},
}} {
t.Run(test.desc, func(t *testing.T) {
ch := make(chan int)
time.AfterFunc(1*time.Millisecond, func() {
test.outside(ch)
})
synctest.Run(func() {
test.inside(ch)
})
})
}
}
func TestChannelMovedOutOfBubble(t *testing.T) {
for _, test := range []struct {
desc string
f func(chan struct{})
wantFatal string
}{{
desc: "receive",
f: func(ch chan struct{}) {
<-ch
},
wantFatal: "receive on synctest channel from outside bubble",
}, {
desc: "send",
f: func(ch chan struct{}) {
ch <- struct{}{}
},
wantFatal: "send on synctest channel from outside bubble",
}, {
desc: "close",
f: func(ch chan struct{}) {
close(ch)
},
wantFatal: "close of synctest channel from outside bubble",
}} {
t.Run(test.desc, func(t *testing.T) {
// Bubbled channel accessed from outside any bubble.
t.Run("outside_bubble", func(t *testing.T) {
wantFatal(t, test.wantFatal, func() {
donec := make(chan struct{})
ch := make(chan chan struct{})
go func() {
defer close(donec)
test.f(<-ch)
}()
synctest.Run(func() {
ch <- make(chan struct{})
})
<-donec
})
})
// Bubbled channel accessed from a different bubble.
t.Run("different_bubble", func(t *testing.T) {
wantFatal(t, test.wantFatal, func() {
donec := make(chan struct{})
ch := make(chan chan struct{})
go func() {
defer close(donec)
c := <-ch
synctest.Run(func() {
test.f(c)
})
}()
synctest.Run(func() {
ch <- make(chan struct{})
})
<-donec
})
})
})
}
}
func TestTimerFromInsideBubble(t *testing.T) {
for _, test := range []struct {
desc string
f func(tm *time.Timer)
wantFatal string
}{{
desc: "read channel",
f: func(tm *time.Timer) {
<-tm.C
},
wantFatal: "receive on synctest channel from outside bubble",
}, {
desc: "Reset",
f: func(tm *time.Timer) {
tm.Reset(1 * time.Second)
},
wantFatal: "reset of synctest timer from outside bubble",
}, {
desc: "Stop",
f: func(tm *time.Timer) {
tm.Stop()
},
wantFatal: "stop of synctest timer from outside bubble",
}} {
t.Run(test.desc, func(t *testing.T) {
wantFatal(t, test.wantFatal, func() {
donec := make(chan struct{})
ch := make(chan *time.Timer)
go func() {
defer close(donec)
test.f(<-ch)
}()
synctest.Run(func() {
tm := time.NewTimer(1 * time.Second)
ch <- tm
})
<-donec
})
})
}
}
func TestDeadlockRoot(t *testing.T) {
defer wantPanic(t, "deadlock: all goroutines in bubble are blocked")
synctest.Run(func() {
select {}
})
}
func TestDeadlockChild(t *testing.T) {
defer wantPanic(t, "deadlock: main bubble goroutine has exited but blocked goroutines remain")
synctest.Run(func() {
go func() {
select {}
}()
})
}
func TestDeadlockTicker(t *testing.T) {
defer wantPanic(t, "deadlock: main bubble goroutine has exited but blocked goroutines remain")
synctest.Run(func() {
go func() {
for range time.Tick(1 * time.Second) {
t.Errorf("ticker unexpectedly ran")
return
}
}()
})
}
func TestCond(t *testing.T) {
synctest.Run(func() {
var mu sync.Mutex
cond := sync.NewCond(&mu)
start := time.Now()
const waitTime = 1 * time.Millisecond
go func() {
// Signal the cond.
time.Sleep(waitTime)
mu.Lock()
cond.Signal()
mu.Unlock()
// Broadcast to the cond.
time.Sleep(waitTime)
mu.Lock()
cond.Broadcast()
mu.Unlock()
}()
// Wait for cond.Signal.
mu.Lock()
cond.Wait()
mu.Unlock()
if got, want := time.Since(start), waitTime; got != want {
t.Errorf("after cond.Signal: time elapsed = %v, want %v", got, want)
}
// Wait for cond.Broadcast in two goroutines.
waiterDone := false
go func() {
mu.Lock()
cond.Wait()
mu.Unlock()
waiterDone = true
}()
mu.Lock()
cond.Wait()
mu.Unlock()
synctest.Wait()
if !waiterDone {
t.Errorf("after cond.Broadcast: waiter not done")
}
if got, want := time.Since(start), 2*waitTime; got != want {
t.Errorf("after cond.Broadcast: time elapsed = %v, want %v", got, want)
}
})
}
func TestIteratorPush(t *testing.T) {
synctest.Run(func() {
seq := func(yield func(time.Time) bool) {
for yield(time.Now()) {
time.Sleep(1 * time.Second)
}
}
var got []time.Time
go func() {
for now := range seq {
got = append(got, now)
if len(got) >= 3 {
break
}
}
}()
want := []time.Time{
time.Now(),
time.Now().Add(1 * time.Second),
time.Now().Add(2 * time.Second),
}
time.Sleep(5 * time.Second)
synctest.Wait()
if !slices.Equal(got, want) {
t.Errorf("got: %v; want: %v", got, want)
}
})
}
func TestIteratorPull(t *testing.T) {
synctest.Run(func() {
seq := func(yield func(time.Time) bool) {
for yield(time.Now()) {
time.Sleep(1 * time.Second)
}
}
var got []time.Time
go func() {
next, stop := iter.Pull(seq)
defer stop()
for len(got) < 3 {
now, _ := next()
got = append(got, now)
}
}()
want := []time.Time{
time.Now(),
time.Now().Add(1 * time.Second),
time.Now().Add(2 * time.Second),
}
time.Sleep(5 * time.Second)
synctest.Wait()
if !slices.Equal(got, want) {
t.Errorf("got: %v; want: %v", got, want)
}
})
}
func TestReflectFuncOf(t *testing.T) {
mkfunc := func(name string, i int) {
reflect.FuncOf([]reflect.Type{
reflect.StructOf([]reflect.StructField{{
Name: name + strconv.Itoa(i),
Type: reflect.TypeOf(0),
}}),
}, nil, false)
}
go func() {
for i := 0; i < 100000; i++ {
mkfunc("A", i)
}
}()
synctest.Run(func() {
for i := 0; i < 100000; i++ {
mkfunc("A", i)
}
})
}
func TestWaitGroupInBubble(t *testing.T) {
synctest.Run(func() {
var wg sync.WaitGroup
wg.Add(1)
const delay = 1 * time.Second
go func() {
time.Sleep(delay)
wg.Done()
}()
start := time.Now()
wg.Wait()
if got := time.Since(start); got != delay {
t.Fatalf("WaitGroup.Wait() took %v, want %v", got, delay)
}
})
}
// https://go.dev/issue/74386
func TestWaitGroupRacingAdds(t *testing.T) {
synctest.Run(func() {
var wg sync.WaitGroup
for range 100 {
wg.Go(func() {})
}
wg.Wait()
})
}
func TestWaitGroupOutOfBubble(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
donec := make(chan struct{})
go synctest.Run(func() {
// Since wg.Add was called outside the bubble, Wait is not durably blocking
// and this waits until wg.Done is called below.
wg.Wait()
close(donec)
})
select {
case <-donec:
t.Fatalf("synctest.Run finished before WaitGroup.Done called")
case <-time.After(1 * time.Millisecond):
}
wg.Done()
<-donec
}
func TestWaitGroupMovedIntoBubble(t *testing.T) {
wantFatal(t, "fatal error: sync: WaitGroup.Add called from inside and outside synctest bubble", func() {
var wg sync.WaitGroup
wg.Add(1)
synctest.Run(func() {
wg.Add(1)
})
})
}
func TestWaitGroupMovedOutOfBubble(t *testing.T) {
wantFatal(t, "fatal error: sync: WaitGroup.Add called from inside and outside synctest bubble", func() {
var wg sync.WaitGroup
synctest.Run(func() {
wg.Add(1)
})
wg.Add(1)
})
}
func TestWaitGroupMovedBetweenBubblesWithNonZeroCount(t *testing.T) {
wantFatal(t, "fatal error: sync: WaitGroup.Add called from multiple synctest bubbles", func() {
var wg sync.WaitGroup
synctest.Run(func() {
wg.Add(1)
})
synctest.Run(func() {
wg.Add(1)
})
})
}
func TestWaitGroupDisassociateInWait(t *testing.T) {
var wg sync.WaitGroup
synctest.Run(func() {
wg.Add(1)
wg.Done()
// Count and waiters are 0, so Wait disassociates the WaitGroup.
wg.Wait()
})
synctest.Run(func() {
// Reusing the WaitGroup is safe, because it is no longer bubbled.
wg.Add(1)
wg.Done()
})
}
func TestWaitGroupDisassociateInAdd(t *testing.T) {
var wg sync.WaitGroup
synctest.Run(func() {
wg.Add(1)
go wg.Wait()
synctest.Wait() // wait for Wait to block
// Count is 0 and waiters != 0, so Done wakes the waiters and
// disassociates the WaitGroup.
wg.Done()
})
synctest.Run(func() {
// Reusing the WaitGroup is safe, because it is no longer bubbled.
wg.Add(1)
wg.Done()
})
}
var testWaitGroupLinkerAllocatedWG sync.WaitGroup
func TestWaitGroupLinkerAllocated(t *testing.T) {
synctest.Run(func() {
// This WaitGroup is probably linker-allocated and has no span,
// so we won't be able to add a special to it associating it with
// this bubble.
//
// Operations on it may not be durably blocking,
// but they shouldn't fail.
testWaitGroupLinkerAllocatedWG.Go(func() {})
testWaitGroupLinkerAllocatedWG.Wait()
})
}
var testWaitGroupHeapAllocatedWG = new(sync.WaitGroup)
func TestWaitGroupHeapAllocated(t *testing.T) {
synctest.Run(func() {
// This package-scoped WaitGroup var should have been heap-allocated,
// so we can associate it with a bubble.
testWaitGroupHeapAllocatedWG.Add(1)
go testWaitGroupHeapAllocatedWG.Wait()
synctest.Wait()
testWaitGroupHeapAllocatedWG.Done()
})
}
// Issue #75134: Many racing bubble associations.
func TestWaitGroupManyBubbles(t *testing.T) {
var wg sync.WaitGroup
for range 100 {
wg.Go(func() {
synctest.Run(func() {
cancelc := make(chan struct{})
var wg2 sync.WaitGroup
for range 100 {
wg2.Go(func() {
<-cancelc
})
}
synctest.Wait()
close(cancelc)
wg2.Wait()
})
})
}
wg.Wait()
}
func TestHappensBefore(t *testing.T) {
// Use two parallel goroutines accessing different vars to ensure that
// we correctly account for multiple goroutines in the bubble.
var v1 int
var v2 int
synctest.Run(func() {
v1++ // 1
v2++ // 1
// Wait returns after these goroutines exit.
go func() {
v1++ // 2
}()
go func() {
v2++ // 2
}()
synctest.Wait()
v1++ // 3
v2++ // 3
// Wait returns after these goroutines block.
ch1 := make(chan struct{})
go func() {
v1++ // 4
<-ch1
}()
go func() {
v2++ // 4
<-ch1
}()
synctest.Wait()
v1++ // 5
v2++ // 5
close(ch1)
// Wait returns after these timers run.
time.AfterFunc(0, func() {
v1++ // 6
})
time.AfterFunc(0, func() {
v2++ // 6
})
synctest.Wait()
v1++ // 7
v2++ // 7
// Wait returns after these timer goroutines block.
ch2 := make(chan struct{})
time.AfterFunc(0, func() {
v1++ // 8
<-ch2
})
time.AfterFunc(0, func() {
v2++ // 8
<-ch2
})
synctest.Wait()
v1++ // 9
v2++ // 9
close(ch2)
})
// This Run happens after the previous Run returns.
synctest.Run(func() {
go func() {
go func() {
v1++ // 10
}()
}()
go func() {
go func() {
v2++ // 10
}()
}()
})
// These tests happen after Run returns.
if got, want := v1, 10; got != want {
t.Errorf("v1 = %v, want %v", got, want)
}
if got, want := v2, 10; got != want {
t.Errorf("v2 = %v, want %v", got, want)
}
}
// https://go.dev/issue/73817
func TestWeak(t *testing.T) {
synctest.Run(func() {
for range 5 {
runtime.GC()
b := make([]byte, 1024)
weak.Make(&b)
}
})
}
func wantPanic(t *testing.T, want string) {
if e := recover(); e != nil {
if got := fmt.Sprint(e); got != want {
t.Errorf("got panic message %q, want %q", got, want)
}
} else {
t.Errorf("got no panic, want one")
}
}
func wantFatal(t *testing.T, want string, f func()) {
t.Helper()
if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
f()
return
}
cmd := testenv.Command(t, testenv.Executable(t), "-test.run=^"+t.Name()+"$")
cmd = testenv.CleanCmdEnv(cmd)
cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
out, err := cmd.CombinedOutput()
if err == nil {
t.Errorf("expected test function to panic, but test returned successfully")
}
if !strings.Contains(string(out), want) {
t.Errorf("wanted test output contaiing %q; got %q", want, string(out))
}
}