test: prove real PipeWire playback callbacks
- commit
- 330b110535e63abb1c8d15b54c141a09797ca5b7
- author
- brice · · 2026-04-10 15:16 UTC
- parent
- 5ee601d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +65 -1
M README.md +1 -1
| @@ -45,4 +45,4 @@ Tagged integration tests require a **real PipeWire runtime/daemon** and verify t | |||
| 45 | 45 | ||
| 46 | 46 | go test -tags integration ./integration ./pipewire -v | |
| 47 | 47 | ||
| 48 | - | These tests exercise state transitions (Idle → Playing → Stopped → Closed) through an actual PipeWire instance. Deeper cleanup internals are primarily covered by unit tests that use mocked dependencies. | |
| 48 | + | The player integration suite exercises state transitions (Idle → Playing → Stopped → Closed) and proves that the real start path reaches the PipeWire process callback, invoking the user-supplied `Fill` callback under a live daemon. Deeper cleanup internals are primarily covered by unit tests that use mocked dependencies. | |
| @@ -45,4 +45,4 @@ Tagged integration tests require a **real PipeWire runtime/daemon** and verify t | |||||
| 45 | 45 | ||||
| 46 | go test -tags integration ./integration ./pipewire -v | 46 | go test -tags integration ./integration ./pipewire -v | ||
| 47 | 47 | ||||
| 48 | - | These tests exercise state transitions (Idle → Playing → Stopped → Closed) through an actual PipeWire instance. Deeper cleanup internals are primarily covered by unit tests that use mocked dependencies. | 48 | + | The player integration suite exercises state transitions (Idle → Playing → Stopped → Closed) and proves that the real start path reaches the PipeWire process callback, invoking the user-supplied `Fill` callback under a live daemon. Deeper cleanup internals are primarily covered by unit tests that use mocked dependencies. |
M integration/player_test.go +64 -0
| @@ -4,6 +4,7 @@ package integration | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "testing" | |
| 7 | + | "time" | |
| 7 | 8 | ||
| 8 | 9 | "github.com/bnema/purego-pipewire/pipewire" | |
| 9 | 10 | ) | |
| @@ -69,3 +70,66 @@ func TestPlayerStartStopClose(t *testing.T) { | |||
| 69 | 70 | t.Errorf("After Close: expected state Closed, got %v", state) | |
| 70 | 71 | } | |
| 71 | 72 | } | |
| 73 | + | ||
| 74 | + | // TestPlayerPlaybackInvokesFill proves that starting a Player under a live | |
| 75 | + | // PipeWire daemon causes the process callback to invoke the user-supplied Fill | |
| 76 | + | // callback. A buffered channel synchronizes the test instead of sleeping. | |
| 77 | + | func TestPlayerPlaybackInvokesFill(t *testing.T) { | |
| 78 | + | initResult, err := pipewire.Init() | |
| 79 | + | if err != nil { | |
| 80 | + | t.Fatalf("Init returned error: %v", err) | |
| 81 | + | } | |
| 82 | + | defer initResult.Deinit() | |
| 83 | + | ||
| 84 | + | filled := make(chan struct{}, 1) // buffered so Fill never blocks | |
| 85 | + | ||
| 86 | + | config := pipewire.PlayerConfig{ | |
| 87 | + | SampleRate: 48000, | |
| 88 | + | Channels: 2, | |
| 89 | + | FramesPerBuffer: 512, | |
| 90 | + | SampleFormat: pipewire.SampleFormatF32, | |
| 91 | + | UnderrunPolicy: pipewire.UnderrunFillSilence, | |
| 92 | + | } | |
| 93 | + | ||
| 94 | + | callbacks := pipewire.PlayerCallbacks{ | |
| 95 | + | Fill: func(buf *pipewire.PCMBuffer) (int, error) { | |
| 96 | + | // Write a small constant so the buffer isn't silent. | |
| 97 | + | const val = 0.001 | |
| 98 | + | for ch := range buf.Samples { | |
| 99 | + | for i := range buf.Samples[ch] { | |
| 100 | + | buf.Samples[ch][i] = val | |
| 101 | + | } | |
| 102 | + | } | |
| 103 | + | // Signal exactly once. | |
| 104 | + | select { | |
| 105 | + | case filled <- struct{}{}: | |
| 106 | + | default: | |
| 107 | + | } | |
| 108 | + | return buf.Frames, nil | |
| 109 | + | }, | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | player, err := pipewire.NewPlayer(config, callbacks) | |
| 113 | + | if err != nil { | |
| 114 | + | t.Fatalf("NewPlayer returned error: %v", err) | |
| 115 | + | } | |
| 116 | + | ||
| 117 | + | if err := player.Start(); err != nil { | |
| 118 | + | t.Fatalf("Start returned error: %v", err) | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | // Wait for the Fill callback to fire at least once. | |
| 122 | + | select { | |
| 123 | + | case <-filled: | |
| 124 | + | // success — the real PipeWire process callback invoked Fill | |
| 125 | + | case <-time.After(5 * time.Second): | |
| 126 | + | t.Fatal("Timed out waiting for Fill callback; PipeWire process callback did not fire") | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | if err := player.Stop(); err != nil { | |
| 130 | + | t.Errorf("Stop returned error: %v", err) | |
| 131 | + | } | |
| 132 | + | if err := player.Close(); err != nil { | |
| 133 | + | t.Errorf("Close returned error: %v", err) | |
| 134 | + | } | |
| 135 | + | } | |
| @@ -4,6 +4,7 @@ package integration | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | "time" | |||
| 7 | 8 | ||||
| 8 | "github.com/bnema/purego-pipewire/pipewire" | 9 | "github.com/bnema/purego-pipewire/pipewire" | ||
| 9 | ) | 10 | ) | ||
| @@ -69,3 +70,66 @@ func TestPlayerStartStopClose(t *testing.T) { | |||||
| 69 | t.Errorf("After Close: expected state Closed, got %v", state) | 70 | t.Errorf("After Close: expected state Closed, got %v", state) | ||
| 70 | } | 71 | } | ||
| 71 | } | 72 | } | ||
| 73 | + | ||||
| 74 | + | // TestPlayerPlaybackInvokesFill proves that starting a Player under a live | |||
| 75 | + | // PipeWire daemon causes the process callback to invoke the user-supplied Fill | |||
| 76 | + | // callback. A buffered channel synchronizes the test instead of sleeping. | |||
| 77 | + | func TestPlayerPlaybackInvokesFill(t *testing.T) { | |||
| 78 | + | initResult, err := pipewire.Init() | |||
| 79 | + | if err != nil { | |||
| 80 | + | t.Fatalf("Init returned error: %v", err) | |||
| 81 | + | } | |||
| 82 | + | defer initResult.Deinit() | |||
| 83 | + | ||||
| 84 | + | filled := make(chan struct{}, 1) // buffered so Fill never blocks | |||
| 85 | + | ||||
| 86 | + | config := pipewire.PlayerConfig{ | |||
| 87 | + | SampleRate: 48000, | |||
| 88 | + | Channels: 2, | |||
| 89 | + | FramesPerBuffer: 512, | |||
| 90 | + | SampleFormat: pipewire.SampleFormatF32, | |||
| 91 | + | UnderrunPolicy: pipewire.UnderrunFillSilence, | |||
| 92 | + | } | |||
| 93 | + | ||||
| 94 | + | callbacks := pipewire.PlayerCallbacks{ | |||
| 95 | + | Fill: func(buf *pipewire.PCMBuffer) (int, error) { | |||
| 96 | + | // Write a small constant so the buffer isn't silent. | |||
| 97 | + | const val = 0.001 | |||
| 98 | + | for ch := range buf.Samples { | |||
| 99 | + | for i := range buf.Samples[ch] { | |||
| 100 | + | buf.Samples[ch][i] = val | |||
| 101 | + | } | |||
| 102 | + | } | |||
| 103 | + | // Signal exactly once. | |||
| 104 | + | select { | |||
| 105 | + | case filled <- struct{}{}: | |||
| 106 | + | default: | |||
| 107 | + | } | |||
| 108 | + | return buf.Frames, nil | |||
| 109 | + | }, | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | player, err := pipewire.NewPlayer(config, callbacks) | |||
| 113 | + | if err != nil { | |||
| 114 | + | t.Fatalf("NewPlayer returned error: %v", err) | |||
| 115 | + | } | |||
| 116 | + | ||||
| 117 | + | if err := player.Start(); err != nil { | |||
| 118 | + | t.Fatalf("Start returned error: %v", err) | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | // Wait for the Fill callback to fire at least once. | |||
| 122 | + | select { | |||
| 123 | + | case <-filled: | |||
| 124 | + | // success — the real PipeWire process callback invoked Fill | |||
| 125 | + | case <-time.After(5 * time.Second): | |||
| 126 | + | t.Fatal("Timed out waiting for Fill callback; PipeWire process callback did not fire") | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | if err := player.Stop(); err != nil { | |||
| 130 | + | t.Errorf("Stop returned error: %v", err) | |||
| 131 | + | } | |||
| 132 | + | if err := player.Close(); err != nil { | |||
| 133 | + | t.Errorf("Close returned error: %v", err) | |||
| 134 | + | } | |||
| 135 | + | } | |||