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

+260 -245
View File
@@ -24,6 +24,7 @@ import (
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"github.com/hajimehoshi/ebiten/v2/audio"
@@ -42,38 +43,40 @@ func teardown() {
// Issue #746
func TestGC(t *testing.T) {
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
p, _ := context.NewPlayer(bytes.NewReader(make([]byte, 4)))
got := audio.PlayersCountForTesting()
if want := 0; got != want {
t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want)
}
p, _ := context.NewPlayer(bytes.NewReader(make([]byte, 4)))
got := audio.PlayersCountForTesting()
if want := 0; got != want {
t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want)
}
p.Play()
got = audio.PlayersCountForTesting()
if want := 1; got != want {
t.Errorf("PlayersCountForTesting() after Play: got: %d, want: %d", got, want)
}
runtime.KeepAlive(p)
p = nil
runtime.GC()
for range 10 {
p.Play()
got = audio.PlayersCountForTesting()
if want := 0; got == want {
return
if want := 1; got != want {
t.Errorf("PlayersCountForTesting() after Play: got: %d, want: %d", got, want)
}
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
runtime.KeepAlive(p)
p = nil
runtime.GC()
for range 10 {
got = audio.PlayersCountForTesting()
if want := 0; got == want {
return
}
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
// 200[ms] should be enough for all the bytes to be consumed. The test runs on the fake
// clock of synctest, so this does not take real time.
time.Sleep(200 * time.Millisecond)
}
// 200[ms] should be enough for all the bytes to be consumed.
// TODO: This is a dirty hack. Would it be possible to use virtual time?
time.Sleep(200 * time.Millisecond)
}
t.Errorf("time out")
t.Errorf("time out")
})
}
type infiniteReader struct{}
@@ -87,35 +90,37 @@ func (i *infiniteReader) Read(p []byte) (int, error) {
// Issue #853
func TestSameSourcePlayers(t *testing.T) {
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
src := &infiniteReader{}
p0, err := context.NewPlayer(src)
if err != nil {
t.Fatal(err)
}
p1, err := context.NewPlayer(src)
if err != nil {
t.Fatal(err)
}
// As the player does not play yet, error doesn't happen.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
p0.Play()
p1.Play()
for range 10 {
if err := audio.UpdateForTesting(); err != nil {
// An error is expected.
return
src := &infiniteReader{}
p0, err := context.NewPlayer(src)
if err != nil {
t.Fatal(err)
}
time.Sleep(200 * time.Millisecond)
}
t.Errorf("time out")
p1, err := context.NewPlayer(src)
if err != nil {
t.Fatal(err)
}
// As the player does not play yet, error doesn't happen.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
p0.Play()
p1.Play()
for range 10 {
if err := audio.UpdateForTesting(); err != nil {
// An error is expected.
return
}
time.Sleep(200 * time.Millisecond)
}
t.Errorf("time out")
})
}
func TestPauseBeforeInit(t *testing.T) {
@@ -166,53 +171,55 @@ func TestNonSeekableSource(t *testing.T) {
// Issue #3438
func TestDeferredDeviceCreation(t *testing.T) {
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
p, err := context.NewPlayer(bytes.NewReader(make([]byte, 4)))
if err != nil {
t.Fatal(err)
}
// Touching a player before the first update must not create the audio device.
p.SetVolume(0.5)
p.Play()
if audio.ContextCreatedForTesting() {
t.Errorf("the audio device must not be created before the first update")
}
// The state set before the device is created is recorded.
if got, want := p.Volume(), 0.5; got != want {
t.Errorf("Volume(): got: %v, want: %v", got, want)
}
if got, want := p.IsPlaying(), true; got != want {
t.Errorf("IsPlaying(): got: %t, want: %t", got, want)
}
if got, want := audio.PlayersCountForTesting(), 1; got != want {
t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want)
}
// The first update creates the audio device.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
if !audio.ContextCreatedForTesting() {
t.Errorf("the audio device must be created after the first update")
}
// The pending player eventually starts playing and finishes, which is only possible
// once the device is created and the player is materialized.
for range 10 {
if audio.PlayersCountForTesting() == 0 {
return
p, err := context.NewPlayer(bytes.NewReader(make([]byte, 4)))
if err != nil {
t.Fatal(err)
}
// Touching a player before the first update must not create the audio device.
p.SetVolume(0.5)
p.Play()
if audio.ContextCreatedForTesting() {
t.Errorf("the audio device must not be created before the first update")
}
// The state set before the device is created is recorded.
if got, want := p.Volume(), 0.5; got != want {
t.Errorf("Volume(): got: %v, want: %v", got, want)
}
if got, want := p.IsPlaying(), true; got != want {
t.Errorf("IsPlaying(): got: %t, want: %t", got, want)
}
if got, want := audio.PlayersCountForTesting(), 1; got != want {
t.Errorf("PlayersCountForTesting(): got: %d, want: %d", got, want)
}
// The first update creates the audio device.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
time.Sleep(200 * time.Millisecond)
}
t.Errorf("time out")
if !audio.ContextCreatedForTesting() {
t.Errorf("the audio device must be created after the first update")
}
// The pending player eventually starts playing and finishes, which is only possible
// once the device is created and the player is materialized.
for range 10 {
if audio.PlayersCountForTesting() == 0 {
return
}
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
time.Sleep(200 * time.Millisecond)
}
t.Errorf("time out")
})
}
// Issue #3438
@@ -450,43 +457,45 @@ func TestPauseAndStopReading(t *testing.T) {
t.Skip("infinite streams in tests cannot be treated well on browsers")
}
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
src := &countingSource{}
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
src := &countingSource{}
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
p.Play()
p.Play()
// The first update creates the audio device and starts the pending player.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
// The first update creates the audio device and starts the pending player.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
waitForRead(t, src, 0)
waitForRead(t, src, 0)
p.PauseAndStopReading()
p.PauseAndStopReading()
if p.IsPlaying() {
t.Error("IsPlaying() after PauseAndStopReading: got: true, want: false")
}
if got, want := audio.PlayersCountForTesting(), 0; got != want {
t.Errorf("PlayersCountForTesting() after PauseAndStopReading: got: %d, want: %d", got, want)
}
if p.IsPlaying() {
t.Error("IsPlaying() after PauseAndStopReading: got: true, want: false")
}
if got, want := audio.PlayersCountForTesting(), 0; got != want {
t.Errorf("PlayersCountForTesting() after PauseAndStopReading: got: %d, want: %d", got, want)
}
// After PauseAndStopReading, the source must not be read.
reads := src.reads.Load()
time.Sleep(100 * time.Millisecond)
if got := src.reads.Load(); got != reads {
t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads)
}
// After PauseAndStopReading, the source must not be read.
reads := src.reads.Load()
time.Sleep(100 * time.Millisecond)
if got := src.reads.Load(); got != reads {
t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads)
}
// The player is reusable after PauseAndStopReading.
p.Play()
waitForRead(t, src, reads)
// The player is reusable after PauseAndStopReading.
p.Play()
waitForRead(t, src, reads)
})
}
// Issue #3510
@@ -495,35 +504,37 @@ func TestPauseAndStopReadingWhilePaused(t *testing.T) {
t.Skip("infinite streams in tests cannot be treated well on browsers")
}
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
src := &countingSource{}
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
src := &countingSource{}
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
p.Play()
p.Play()
// The first update creates the audio device and starts the pending player.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
// The first update creates the audio device and starts the pending player.
if err := audio.UpdateForTesting(); err != nil {
t.Error(err)
}
reads := waitForRead(t, src, 0)
reads := waitForRead(t, src, 0)
// A paused player keeps reading the source to fill its buffer.
p.Pause()
waitForRead(t, src, reads)
// A paused player keeps reading the source to fill its buffer.
p.Pause()
waitForRead(t, src, reads)
// PauseAndStopReading must stop the reads even though the player is already paused.
p.PauseAndStopReading()
reads = src.reads.Load()
time.Sleep(100 * time.Millisecond)
if got := src.reads.Load(); got != reads {
t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads)
}
// PauseAndStopReading must stop the reads even though the player is already paused.
p.PauseAndStopReading()
reads = src.reads.Load()
time.Sleep(100 * time.Millisecond)
if got := src.reads.Load(); got != reads {
t.Errorf("the source was read after PauseAndStopReading: got: %d reads, want: %d", got, reads)
}
})
}
// Issue #3510
@@ -635,51 +646,53 @@ func TestSetBufferSize(t *testing.T) {
}
func TestPositionNotGrowingAfterFinished(t *testing.T) {
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
// 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio.
src := bytes.NewReader(make([]byte, 44100*8*4))
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
p.Play()
// Wait until the player finishes its source. The deadline is generous because time.Sleep has
// a several-millisecond floor on browsers, which slows draining the source down.
var finished bool
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if err := audio.UpdateForTesting(); err != nil {
// 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio.
src := bytes.NewReader(make([]byte, 44100*8*4))
p, err := context.NewPlayerF32(src)
if err != nil {
t.Fatal(err)
}
if !p.IsPlaying() {
finished = true
break
}
time.Sleep(5 * time.Millisecond)
}
if !finished {
t.Fatal("time out: the player did not finish")
}
end := p.Position()
if got, want := end, 4*time.Second; got < want {
t.Errorf("Position() after the player finished: got: %v, want: at least %v", got, want)
}
// Play on a finished player does nothing, so the position must not grow anymore.
for range 20 {
p.Play()
if err := audio.UpdateForTesting(); err != nil {
t.Fatal(err)
// Wait until the player finishes its source. The test runs on the fake clock of synctest, so
// the deadline does not take real time.
var finished bool
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
if err := audio.UpdateForTesting(); err != nil {
t.Fatal(err)
}
if !p.IsPlaying() {
finished = true
break
}
time.Sleep(5 * time.Millisecond)
}
time.Sleep(5 * time.Millisecond)
}
if got := p.Position(); got != end {
t.Errorf("position grew after the player finished: %v -> %v", end, got)
}
if !finished {
t.Fatal("time out: the player did not finish")
}
end := p.Position()
if got, want := end, 4*time.Second; got < want {
t.Errorf("Position() after the player finished: got: %v, want: at least %v", got, want)
}
// Play on a finished player does nothing, so the position must not grow anymore.
for range 20 {
p.Play()
if err := audio.UpdateForTesting(); err != nil {
t.Fatal(err)
}
time.Sleep(5 * time.Millisecond)
}
if got := p.Position(); got != end {
t.Errorf("position grew after the player finished: %v -> %v", end, got)
}
})
}
func TestNewPlayerWithSourceOfClosedPlayer(t *testing.T) {
@@ -797,70 +810,72 @@ func TestPlayerErrorDoesNotStopOtherPlayers(t *testing.T) {
t.Skip("infinite streams in tests cannot be treated well on browsers")
}
setup()
defer teardown()
synctest.Test(t, func(t *testing.T) {
setup()
defer teardown()
// The first update creates the audio device, so that the players below play for real.
if err := audio.UpdateForTesting(); err != nil {
t.Fatal(err)
}
failing, err := context.NewPlayerF32(&failingSource{remaining: 4096 * 4})
if err != nil {
t.Fatal(err)
}
// 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio.
healthy, err := context.NewPlayerF32(audio.NewInfiniteLoopF32(bytes.NewReader(make([]byte, 44100*8)), 44100*8))
if err != nil {
t.Fatal(err)
}
failing.Play()
healthy.Play()
// Wait until the failing player's source fails and the failure is reported as the context
// error.
var failed bool
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
// The first update creates the audio device, so that the players below play for real.
if err := audio.UpdateForTesting(); err != nil {
if !errors.Is(err, errSourceFailed) {
t.Fatal(err)
}
failed = true
break
t.Fatal(err)
}
time.Sleep(5 * time.Millisecond)
}
if !failed {
t.Fatal("time out: the failing player's error was not reported")
}
// The failed player is no longer tracked as playing, so that its error is reported once and
// the healthy player is the only playing player left.
deadline = time.Now().Add(time.Second)
for audio.PlayersCountForTesting() != 1 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if got, want := audio.PlayersCountForTesting(), 1; got != want {
t.Errorf("PlayersCountForTesting() after a player failed: got: %d, want: %d", got, want)
}
failing, err := context.NewPlayerF32(&failingSource{remaining: 4096 * 4})
if err != nil {
t.Fatal(err)
}
// 44100 [Hz] * 8 [bytes/sample] is one second of 32bit float stereo audio.
healthy, err := context.NewPlayerF32(audio.NewInfiniteLoopF32(bytes.NewReader(make([]byte, 44100*8)), 44100*8))
if err != nil {
t.Fatal(err)
}
// The healthy player must keep playing and its position must keep advancing: one player's
// failure must not stop the context from updating the other players.
if !healthy.IsPlaying() {
t.Error("IsPlaying() of the healthy player after another player failed: got: false, want: true")
}
pos0 := healthy.Position()
time.Sleep(100 * time.Millisecond)
if pos1 := healthy.Position(); pos1 <= pos0 {
t.Errorf("Position() of the healthy player did not advance after another player failed: %v -> %v", pos0, pos1)
}
failing.Play()
healthy.Play()
// The context error is sticky.
if err := audio.UpdateForTesting(); !errors.Is(err, errSourceFailed) {
t.Errorf("UpdateForTesting() after a player failed: got: %v, want: %v", err, errSourceFailed)
}
// Wait until the failing player's source fails and the failure is reported as the context
// error.
var failed bool
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
if err := audio.UpdateForTesting(); err != nil {
if !errors.Is(err, errSourceFailed) {
t.Fatal(err)
}
failed = true
break
}
time.Sleep(5 * time.Millisecond)
}
if !failed {
t.Fatal("time out: the failing player's error was not reported")
}
// The failed player is no longer tracked as playing, so that its error is reported once and
// the healthy player is the only playing player left.
deadline = time.Now().Add(time.Second)
for audio.PlayersCountForTesting() != 1 && time.Now().Before(deadline) {
time.Sleep(5 * time.Millisecond)
}
if got, want := audio.PlayersCountForTesting(), 1; got != want {
t.Errorf("PlayersCountForTesting() after a player failed: got: %d, want: %d", got, want)
}
// The healthy player must keep playing and its position must keep advancing: one player's
// failure must not stop the context from updating the other players.
if !healthy.IsPlaying() {
t.Error("IsPlaying() of the healthy player after another player failed: got: false, want: true")
}
pos0 := healthy.Position()
time.Sleep(100 * time.Millisecond)
if pos1 := healthy.Position(); pos1 <= pos0 {
t.Errorf("Position() of the healthy player did not advance after another player failed: %v -> %v", pos0, pos1)
}
// The context error is sticky.
if err := audio.UpdateForTesting(); !errors.Is(err, errSourceFailed) {
t.Errorf("UpdateForTesting() after a player failed: got: %v, want: %v", err, errSourceFailed)
}
})
}
func TestPlayOnIdleContext(t *testing.T) {