audio: run the tests waiting on real time on the fake clock of synctest

TestGC, TestSameSourcePlayers, TestDeferredDeviceCreation,
TestPauseAndStopReading, TestPauseAndStopReadingWhilePaused,
TestPositionNotGrowingAfterFinished and
TestPlayerErrorDoesNotStopOtherPlayers now run in a synctest bubble, so
their sleeps and deadlines take no real time.

A bubble must not leave goroutines behind, so ResetContextForTesting
now stops the goroutine updating the players and the dummy players
reading their sources, and waits for them to exit. Before, every test
leaked them.

Updates #3716
This commit is contained in:
dualfroz committed 2026-10-03 16:06:04 +02:00
1 parent 87bac3749b
commit d22b1a2b09
3 files changed
+322 -257

No files matched your search

+42 -4
View File
@@ -27,6 +27,10 @@ type (
suspendErr error
resumeErr error
// players are the players created since the context was reset last. ResetContextForTesting
// stops them, so that a player reading an infinite source does not keep reading after its test.
players []*dummyPlayer
mu sync.Mutex
}
dummyPlayer struct {
@@ -45,6 +49,9 @@ type (
// readGen is incremented by PauseAndStopReading to stop the goroutine reading r.
readGen int
// readers tracks the goroutines reading r, so that a test can wait for them to stop.
readers sync.WaitGroup
// err is the first non-EOF error the source returned. Like the real players, a player
// whose source failed stops for good and reports the error from Err.
err error
@@ -54,10 +61,27 @@ type (
)
func (c *dummyContext) NewPlayer(r io.Reader) player {
return &dummyPlayer{
p := &dummyPlayer{
r: r,
volume: 1,
}
c.mu.Lock()
defer c.mu.Unlock()
c.players = append(c.players, p)
return p
}
// stopPlayers stops the players' goroutines reading their sources, and waits for them to exit.
func (c *dummyContext) stopPlayers() {
c.mu.Lock()
players := c.players
c.players = nil
c.mu.Unlock()
for _, p := range players {
p.PauseAndStopReading()
p.readers.Wait()
}
}
func (c *dummyContext) MaxBufferSize() int {
@@ -95,7 +119,7 @@ func (p *dummyPlayer) Play() {
}
p.playing = true
gen := p.readGen
go func() {
p.readers.Go(func() {
var buf [4096]byte
for {
stopped, err := p.readOnce(gen, buf[:])
@@ -114,7 +138,7 @@ func (p *dummyPlayer) Play() {
if p.playing {
p.eof = true
}
}()
})
}
// readOnce performs one read from the source with the mutex held, so that PauseAndStopReading waits
@@ -324,10 +348,24 @@ func ContextCreatedForTesting() bool {
return c.playerFactory.currentContext() != nil
}
// ResetContextForTesting discards the current context, so that a test can create a new one.
// ResetContextForTesting waits for the goroutines the context and its players started to exit, so
// that they do not outlive the test.
func ResetContextForTesting() {
theContextLock.Lock()
defer theContextLock.Unlock()
c := theContext
theContext = nil
theContextLock.Unlock()
if c != nil {
c.m.Lock()
c.disposed = true
c.cond.Broadcast()
c.m.Unlock()
c.updater.Wait()
}
dummyContextForTesting.stopPlayers()
}
func (i *InfiniteLoop) SetNoBlendForTesting(value bool) {