detached 330b110

test: prove real PipeWire playback callbacks

commit
330b110535e63abb1c8d15b54c141a09797ca5b7
author
brice · · 2026-04-10 15:16 UTC
parent
5ee601d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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
4545
4646 go test -tags integration ./integration ./pipewire -v
4747
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 -v46 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
44
55 import (
66 "testing"
7+ "time"
78
89 "github.com/bnema/purego-pipewire/pipewire"
910 )
@@ -69,3 +70,66 @@ func TestPlayerStartStopClose(t *testing.T) {
6970 t.Errorf("After Close: expected state Closed, got %v", state)
7071 }
7172 }
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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close