diff --git a/src/internal/synctest/synctest_test.go b/src/internal/synctest/synctest_test.go index 010679b070f87a3167df8d6e0d689cc89d926213..c0e126e3fcd77be7d2923a9cb9b885c9ea130ce8 100644 --- a/src/internal/synctest/synctest_test.go +++ b/src/internal/synctest/synctest_test.go @@ -191,6 +191,18 @@ func TestTimeAfter(t *testing.T) { }) } +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() { @@ -308,6 +320,18 @@ func TestDeadlockChild(t *testing.T) { }) } +func TestDeadlockTicker(t *testing.T) { + defer wantPanic(t, "deadlock: all goroutines in bubble are blocked") + 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 diff --git a/src/runtime/synctest.go b/src/runtime/synctest.go index b197758ad9348274d0ef389d627bcf1e37fcfdaf..36d6fa67c7158656516e8ee1794d3147f11796de 100644 --- a/src/runtime/synctest.go +++ b/src/runtime/synctest.go @@ -5,6 +5,7 @@ package runtime import ( + "internal/runtime/sys" "unsafe" ) @@ -15,7 +16,9 @@ type synctestGroup struct { now int64 // current fake time root *g // caller of synctest.Run waiter *g // caller of synctest.Wait + main *g // goroutine started by synctest.Run waiting bool // true if a goroutine is calling synctest.Wait + done bool // true if main has exited // The group is active (not blocked) so long as running > 0 || active > 0. // @@ -60,6 +63,9 @@ func (sg *synctestGroup) changegstatus(gp *g, oldval, newval uint32) { case _Gdead: isRunning = false totalDelta-- + if gp == sg.main { + sg.done = true + } case _Gwaiting: if gp.waitreason.isIdleInSynctest() { isRunning = false @@ -167,24 +173,32 @@ func synctestRun(f func()) { if gp.syncGroup != nil { panic("synctest.Run called from within a synctest bubble") } - gp.syncGroup = &synctestGroup{ + sg := &synctestGroup{ total: 1, running: 1, root: gp, } const synctestBaseTime = 946684800000000000 // midnight UTC 2000-01-01 - gp.syncGroup.now = synctestBaseTime - gp.syncGroup.timers.syncGroup = gp.syncGroup - lockInit(&gp.syncGroup.mu, lockRankSynctest) - lockInit(&gp.syncGroup.timers.mu, lockRankTimers) + sg.now = synctestBaseTime + sg.timers.syncGroup = sg + lockInit(&sg.mu, lockRankSynctest) + lockInit(&sg.timers.mu, lockRankTimers) + + gp.syncGroup = sg defer func() { gp.syncGroup = nil }() - fv := *(**funcval)(unsafe.Pointer(&f)) - newproc(fv) + // This is newproc, but also records the new g in sg.main. + pc := sys.GetCallerPC() + systemstack(func() { + fv := *(**funcval)(unsafe.Pointer(&f)) + sg.main = newproc1(fv, gp, pc, false, waitReasonZero) + pp := getg().m.p.ptr() + runqput(pp, sg.main, true) + wakep() + }) - sg := gp.syncGroup lock(&sg.mu) sg.active++ for { @@ -209,6 +223,10 @@ func synctestRun(f func()) { if next < sg.now { throw("time went backwards") } + if sg.done { + // Time stops once the bubble's main goroutine has exited. + break + } sg.now = next } diff --git a/src/testing/synctest/synctest.go b/src/testing/synctest/synctest.go index 90efc789de972ea50c831f4c8dd05ca996ca7390..1b1aef2e79f138036c853f4159311f6559c25285 100644 --- a/src/testing/synctest/synctest.go +++ b/src/testing/synctest/synctest.go @@ -28,7 +28,10 @@ import ( // goroutines are blocked and return after the bubble's clock has // advanced. See [Wait] for the specific definition of blocked. // -// If every goroutine is blocked and there are no timers scheduled, +// Time stops advancing when f returns. +// +// If every goroutine is blocked and either +// no timers are scheduled or f has returned, // Run panics. // // Channels, time.Timers, and time.Tickers created within the bubble