| // 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)) |
| } |
| } |