detached 70342dc

fix: preserve PipeWire cleanup error details

commit
70342dc0892ce54dcff87afa8b8db91960e95592
author
brice · · 2026-04-10 04:21 UTC
parent
c41bf8c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +244 -22
M internal/capi/stream_ops.go +24 -11
@@ -1,7 +1,6 @@
11 package capi
22
33 import (
4- "errors"
54 "fmt"
65 "sync"
76 "unsafe"
@@ -9,10 +8,20 @@ import (
98 portout "github.com/bnema/purego-pipewire/internal/ports/out"
109 )
1110
11+// PWError represents a PipeWire C API error with the function name and return code.
12+type PWError struct {
13+ Func string
14+ Code int32
15+}
16+
17+func (e *PWError) Error() string {
18+ return fmt.Sprintf("%s failed: %d", e.Func, e.Code)
19+}
20+
1221 var (
13- errStreamCreate = errors.New("pw_stream_new_simple returned nil")
14- errStreamConnect = errors.New("pw_stream_connect failed")
15- errMainLoopCreate = errors.New("pw_main_loop_new returned nil")
22+ errStreamCreate = &PWError{Func: "pw_stream_new_simple", Code: 0}
23+ errStreamConnect = &PWError{Func: "pw_stream_connect", Code: 0}
24+ errMainLoopCreate = &PWError{Func: "pw_main_loop_new", Code: 0}
1625 )
1726
1827 const pwVersionStreamEvents = 2
@@ -82,7 +91,7 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
8291 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
8392 ret := pw_stream_connect(streamPtr, pwDirectionOutput, pwIDAny, pwStreamFlagAutoConnectMapBuffers, nil, 0)
8493 if ret < 0 {
85- return errStreamConnect
94+ return &PWError{Func: "pw_stream_connect", Code: ret}
8695 }
8796 return nil
8897 }
@@ -90,7 +99,7 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
9099 func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
91100 ret := pw_stream_set_active(streamPtr, active)
92101 if ret < 0 {
93- return fmt.Errorf("pw_stream_set_active failed: %d", ret)
102+ return &PWError{Func: "pw_stream_set_active", Code: ret}
94103 }
95104 return nil
96105 }
@@ -102,15 +111,19 @@ func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
102111 func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
103112 ret := pw_stream_queue_buffer(streamPtr, bufPtr)
104113 if ret < 0 {
105- return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)
114+ return &PWError{Func: "pw_stream_queue_buffer", Code: ret}
106115 }
107116 return nil
108117 }
109118
110-// DisconnectStream intentionally ignores the PipeWire return code.
111-// Close-path cleanup is best-effort here, and destroy is the definitive release.
112-func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {
113- pw_stream_disconnect(streamPtr)
119+// DisconnectStream disconnects the stream from its port.
120+// Returns a PWError if the operation fails.
121+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) error {
122+ ret := pw_stream_disconnect(streamPtr)
123+ if ret < 0 {
124+ return &PWError{Func: "pw_stream_disconnect", Code: ret}
125+ }
126+ return nil
114127 }
115128
116129 func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
@@ -1,7 +1,6 @@
1 package capi1 package capi
2 2
3 import (3 import (
4- "errors"
5 "fmt"4 "fmt"
6 "sync"5 "sync"
7 "unsafe"6 "unsafe"
@@ -9,10 +8,20 @@ import (
9 portout "github.com/bnema/purego-pipewire/internal/ports/out"8 portout "github.com/bnema/purego-pipewire/internal/ports/out"
10 )9 )
11 10
11+// PWError represents a PipeWire C API error with the function name and return code.
12+type PWError struct {
13+ Func string
14+ Code int32
15+}
16+
17+func (e *PWError) Error() string {
18+ return fmt.Sprintf("%s failed: %d", e.Func, e.Code)
19+}
20+
12 var (21 var (
13- errStreamCreate = errors.New("pw_stream_new_simple returned nil")22+ errStreamCreate = &PWError{Func: "pw_stream_new_simple", Code: 0}
14- errStreamConnect = errors.New("pw_stream_connect failed")23+ errStreamConnect = &PWError{Func: "pw_stream_connect", Code: 0}
15- errMainLoopCreate = errors.New("pw_main_loop_new returned nil")24+ errMainLoopCreate = &PWError{Func: "pw_main_loop_new", Code: 0}
16 )25 )
17 26
18 const pwVersionStreamEvents = 227 const pwVersionStreamEvents = 2
@@ -82,7 +91,7 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
82 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x0491 // PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS = 0x01 | 0x04
83 ret := pw_stream_connect(streamPtr, pwDirectionOutput, pwIDAny, pwStreamFlagAutoConnectMapBuffers, nil, 0)92 ret := pw_stream_connect(streamPtr, pwDirectionOutput, pwIDAny, pwStreamFlagAutoConnectMapBuffers, nil, 0)
84 if ret < 0 {93 if ret < 0 {
85- return errStreamConnect94+ return &PWError{Func: "pw_stream_connect", Code: ret}
86 }95 }
87 return nil96 return nil
88 }97 }
@@ -90,7 +99,7 @@ func (s *streamOpsImpl) ConnectPlaybackStream(streamPtr unsafe.Pointer) error {
90 func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {99 func (s *streamOpsImpl) SetStreamActive(streamPtr unsafe.Pointer, active bool) error {
91 ret := pw_stream_set_active(streamPtr, active)100 ret := pw_stream_set_active(streamPtr, active)
92 if ret < 0 {101 if ret < 0 {
93- return fmt.Errorf("pw_stream_set_active failed: %d", ret)102+ return &PWError{Func: "pw_stream_set_active", Code: ret}
94 }103 }
95 return nil104 return nil
96 }105 }
@@ -102,15 +111,19 @@ func (s *streamOpsImpl) DequeueBuffer(streamPtr unsafe.Pointer) unsafe.Pointer {
102 func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {111 func (s *streamOpsImpl) QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error {
103 ret := pw_stream_queue_buffer(streamPtr, bufPtr)112 ret := pw_stream_queue_buffer(streamPtr, bufPtr)
104 if ret < 0 {113 if ret < 0 {
105- return fmt.Errorf("pw_stream_queue_buffer failed: %d", ret)114+ return &PWError{Func: "pw_stream_queue_buffer", Code: ret}
106 }115 }
107 return nil116 return nil
108 }117 }
109 118
110-// DisconnectStream intentionally ignores the PipeWire return code.119+// DisconnectStream disconnects the stream from its port.
111-// Close-path cleanup is best-effort here, and destroy is the definitive release.120+// Returns a PWError if the operation fails.
112-func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) {121+func (s *streamOpsImpl) DisconnectStream(streamPtr unsafe.Pointer) error {
113- pw_stream_disconnect(streamPtr)122+ ret := pw_stream_disconnect(streamPtr)
123+ if ret < 0 {
124+ return &PWError{Func: "pw_stream_disconnect", Code: ret}
125+ }
126+ return nil
114 }127 }
115 128
116 func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {129 func (s *streamOpsImpl) DestroyStream(streamPtr unsafe.Pointer) {
M internal/capi/stream_ops_test.go +141 -0
@@ -1,6 +1,7 @@
11 package capi
22
33 import (
4+ "errors"
45 "testing"
56 "unsafe"
67 )
@@ -118,3 +119,143 @@ func TestQuitMainLoopCallsPwMainLoopQuit(t *testing.T) {
118119 t.Fatalf("pw_main_loop_quit received %v, want %v", receivedPtr, fakeLoop)
119120 }
120121 }
122+
123+// TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn verifies that
124+// ConnectPlaybackStream returns a PWError with function name and return code.
125+func TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
126+ origConnect := pw_stream_connect
127+ t.Cleanup(func() { pw_stream_connect = origConnect })
128+
129+ pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
130+ return -22 // EINVAL
131+ }
132+
133+ ops := &streamOpsImpl{}
134+ fakePtr := unsafe.Pointer(uintptr(0x1234))
135+
136+ err := ops.ConnectPlaybackStream(fakePtr)
137+ if err == nil {
138+ t.Fatal("expected error for negative return code, got nil")
139+ }
140+
141+ var pwErr *PWError
142+ if !errors.As(err, &pwErr) {
143+ t.Fatalf("expected *PWError, got %T", err)
144+ }
145+ if pwErr.Func != "pw_stream_connect" {
146+ t.Errorf("expected Func='pw_stream_connect', got '%s'", pwErr.Func)
147+ }
148+ if pwErr.Code != -22 {
149+ t.Errorf("expected Code=-22, got %d", pwErr.Code)
150+ }
151+}
152+
153+// TestSetStreamActiveReturnsTypedErrorOnNegativeReturn verifies that
154+// SetStreamActive returns a PWError with function name and return code.
155+func TestSetStreamActiveReturnsTypedErrorOnNegativeReturn(t *testing.T) {
156+ origSetActive := pw_stream_set_active
157+ t.Cleanup(func() { pw_stream_set_active = origSetActive })
158+
159+ pw_stream_set_active = func(stream unsafe.Pointer, active bool) int32 {
160+ return -9 // EBADF
161+ }
162+
163+ ops := &streamOpsImpl{}
164+ fakePtr := unsafe.Pointer(uintptr(0x5678))
165+
166+ err := ops.SetStreamActive(fakePtr, true)
167+ if err == nil {
168+ t.Fatal("expected error for negative return code, got nil")
169+ }
170+
171+ var pwErr *PWError
172+ if !errors.As(err, &pwErr) {
173+ t.Fatalf("expected *PWError, got %T", err)
174+ }
175+ if pwErr.Func != "pw_stream_set_active" {
176+ t.Errorf("expected Func='pw_stream_set_active', got '%s'", pwErr.Func)
177+ }
178+ if pwErr.Code != -9 {
179+ t.Errorf("expected Code=-9, got %d", pwErr.Code)
180+ }
181+}
182+
183+// TestQueueBufferReturnsTypedErrorOnNegativeReturn verifies that
184+// QueueBuffer returns a PWError with function name and return code.
185+func TestQueueBufferReturnsTypedErrorOnNegativeReturn(t *testing.T) {
186+ origQueue := pw_stream_queue_buffer
187+ t.Cleanup(func() { pw_stream_queue_buffer = origQueue })
188+
189+ pw_stream_queue_buffer = func(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
190+ return -5 // EIO
191+ }
192+
193+ ops := &streamOpsImpl{}
194+ fakePtr := unsafe.Pointer(uintptr(0x9ABC))
195+ fakeBuf := unsafe.Pointer(uintptr(0xDEF0))
196+
197+ err := ops.QueueBuffer(fakePtr, fakeBuf)
198+ if err == nil {
199+ t.Fatal("expected error for negative return code, got nil")
200+ }
201+
202+ var pwErr *PWError
203+ if !errors.As(err, &pwErr) {
204+ t.Fatalf("expected *PWError, got %T", err)
205+ }
206+ if pwErr.Func != "pw_stream_queue_buffer" {
207+ t.Errorf("expected Func='pw_stream_queue_buffer', got '%s'", pwErr.Func)
208+ }
209+ if pwErr.Code != -5 {
210+ t.Errorf("expected Code=-5, got %d", pwErr.Code)
211+ }
212+}
213+
214+// TestDisconnectStreamReturnsTypedErrorOnNegativeReturn verifies that
215+// DisconnectStream returns a PWError with function name and return code.
216+func TestDisconnectStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
217+ origDisconnect := pw_stream_disconnect
218+ t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
219+
220+ pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
221+ return -19 // ENODEV
222+ }
223+
224+ ops := &streamOpsImpl{}
225+ fakePtr := unsafe.Pointer(uintptr(0x1111))
226+
227+ err := ops.DisconnectStream(fakePtr)
228+ if err == nil {
229+ t.Fatal("expected error for negative return code, got nil")
230+ }
231+
232+ var pwErr *PWError
233+ if !errors.As(err, &pwErr) {
234+ t.Fatalf("expected *PWError, got %T", err)
235+ }
236+ if pwErr.Func != "pw_stream_disconnect" {
237+ t.Errorf("expected Func='pw_stream_disconnect', got '%s'", pwErr.Func)
238+ }
239+ if pwErr.Code != -19 {
240+ t.Errorf("expected Code=-19, got %d", pwErr.Code)
241+ }
242+}
243+
244+// TestDisconnectStreamReturnsNilOnSuccess verifies that DisconnectStream
245+// returns nil when the return code is non-negative.
246+func TestDisconnectStreamReturnsNilOnSuccess(t *testing.T) {
247+ origDisconnect := pw_stream_disconnect
248+ t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
249+
250+ pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
251+ return 0 // Success
252+ }
253+
254+ ops := &streamOpsImpl{}
255+ fakePtr := unsafe.Pointer(uintptr(0x2222))
256+
257+ err := ops.DisconnectStream(fakePtr)
258+ if err != nil {
259+ t.Fatalf("expected nil on success, got %v", err)
260+ }
261+}
@@ -1,6 +1,7 @@
1 package capi1 package capi
2 2
3 import (3 import (
4+ "errors"
4 "testing"5 "testing"
5 "unsafe"6 "unsafe"
6 )7 )
@@ -118,3 +119,143 @@ func TestQuitMainLoopCallsPwMainLoopQuit(t *testing.T) {
118 t.Fatalf("pw_main_loop_quit received %v, want %v", receivedPtr, fakeLoop)119 t.Fatalf("pw_main_loop_quit received %v, want %v", receivedPtr, fakeLoop)
119 }120 }
120 }121 }
122+
123+// TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn verifies that
124+// ConnectPlaybackStream returns a PWError with function name and return code.
125+func TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
126+ origConnect := pw_stream_connect
127+ t.Cleanup(func() { pw_stream_connect = origConnect })
128+
129+ pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
130+ return -22 // EINVAL
131+ }
132+
133+ ops := &streamOpsImpl{}
134+ fakePtr := unsafe.Pointer(uintptr(0x1234))
135+
136+ err := ops.ConnectPlaybackStream(fakePtr)
137+ if err == nil {
138+ t.Fatal("expected error for negative return code, got nil")
139+ }
140+
141+ var pwErr *PWError
142+ if !errors.As(err, &pwErr) {
143+ t.Fatalf("expected *PWError, got %T", err)
144+ }
145+ if pwErr.Func != "pw_stream_connect" {
146+ t.Errorf("expected Func='pw_stream_connect', got '%s'", pwErr.Func)
147+ }
148+ if pwErr.Code != -22 {
149+ t.Errorf("expected Code=-22, got %d", pwErr.Code)
150+ }
151+}
152+
153+// TestSetStreamActiveReturnsTypedErrorOnNegativeReturn verifies that
154+// SetStreamActive returns a PWError with function name and return code.
155+func TestSetStreamActiveReturnsTypedErrorOnNegativeReturn(t *testing.T) {
156+ origSetActive := pw_stream_set_active
157+ t.Cleanup(func() { pw_stream_set_active = origSetActive })
158+
159+ pw_stream_set_active = func(stream unsafe.Pointer, active bool) int32 {
160+ return -9 // EBADF
161+ }
162+
163+ ops := &streamOpsImpl{}
164+ fakePtr := unsafe.Pointer(uintptr(0x5678))
165+
166+ err := ops.SetStreamActive(fakePtr, true)
167+ if err == nil {
168+ t.Fatal("expected error for negative return code, got nil")
169+ }
170+
171+ var pwErr *PWError
172+ if !errors.As(err, &pwErr) {
173+ t.Fatalf("expected *PWError, got %T", err)
174+ }
175+ if pwErr.Func != "pw_stream_set_active" {
176+ t.Errorf("expected Func='pw_stream_set_active', got '%s'", pwErr.Func)
177+ }
178+ if pwErr.Code != -9 {
179+ t.Errorf("expected Code=-9, got %d", pwErr.Code)
180+ }
181+}
182+
183+// TestQueueBufferReturnsTypedErrorOnNegativeReturn verifies that
184+// QueueBuffer returns a PWError with function name and return code.
185+func TestQueueBufferReturnsTypedErrorOnNegativeReturn(t *testing.T) {
186+ origQueue := pw_stream_queue_buffer
187+ t.Cleanup(func() { pw_stream_queue_buffer = origQueue })
188+
189+ pw_stream_queue_buffer = func(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
190+ return -5 // EIO
191+ }
192+
193+ ops := &streamOpsImpl{}
194+ fakePtr := unsafe.Pointer(uintptr(0x9ABC))
195+ fakeBuf := unsafe.Pointer(uintptr(0xDEF0))
196+
197+ err := ops.QueueBuffer(fakePtr, fakeBuf)
198+ if err == nil {
199+ t.Fatal("expected error for negative return code, got nil")
200+ }
201+
202+ var pwErr *PWError
203+ if !errors.As(err, &pwErr) {
204+ t.Fatalf("expected *PWError, got %T", err)
205+ }
206+ if pwErr.Func != "pw_stream_queue_buffer" {
207+ t.Errorf("expected Func='pw_stream_queue_buffer', got '%s'", pwErr.Func)
208+ }
209+ if pwErr.Code != -5 {
210+ t.Errorf("expected Code=-5, got %d", pwErr.Code)
211+ }
212+}
213+
214+// TestDisconnectStreamReturnsTypedErrorOnNegativeReturn verifies that
215+// DisconnectStream returns a PWError with function name and return code.
216+func TestDisconnectStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
217+ origDisconnect := pw_stream_disconnect
218+ t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
219+
220+ pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
221+ return -19 // ENODEV
222+ }
223+
224+ ops := &streamOpsImpl{}
225+ fakePtr := unsafe.Pointer(uintptr(0x1111))
226+
227+ err := ops.DisconnectStream(fakePtr)
228+ if err == nil {
229+ t.Fatal("expected error for negative return code, got nil")
230+ }
231+
232+ var pwErr *PWError
233+ if !errors.As(err, &pwErr) {
234+ t.Fatalf("expected *PWError, got %T", err)
235+ }
236+ if pwErr.Func != "pw_stream_disconnect" {
237+ t.Errorf("expected Func='pw_stream_disconnect', got '%s'", pwErr.Func)
238+ }
239+ if pwErr.Code != -19 {
240+ t.Errorf("expected Code=-19, got %d", pwErr.Code)
241+ }
242+}
243+
244+// TestDisconnectStreamReturnsNilOnSuccess verifies that DisconnectStream
245+// returns nil when the return code is non-negative.
246+func TestDisconnectStreamReturnsNilOnSuccess(t *testing.T) {
247+ origDisconnect := pw_stream_disconnect
248+ t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
249+
250+ pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
251+ return 0 // Success
252+ }
253+
254+ ops := &streamOpsImpl{}
255+ fakePtr := unsafe.Pointer(uintptr(0x2222))
256+
257+ err := ops.DisconnectStream(fakePtr)
258+ if err != nil {
259+ t.Fatalf("expected nil on success, got %v", err)
260+ }
261+}
M internal/core/player.go +4 -1
@@ -220,6 +220,8 @@ func (p *player) deactivateStream() error {
220220
221221 // teardown releases player-owned stream and main-loop resources through
222222 // StreamOps. Safe to call when fields are nil or on repeated invocation.
223+// DisconnectStream is called before DestroyStream as best-effort cleanup;
224+// if disconnect fails, teardown continues (destroy is the definitive release).
223225 func (p *player) teardown() {
224226 p.mu.Lock()
225227 streamOps := p.streamOps
@@ -233,8 +235,9 @@ func (p *player) teardown() {
233235 return
234236 }
235237
236- // Destroy stream first (it depends on the loop).
238+ // Disconnect then destroy stream (disconnect is best-effort, destroy is definitive).
237239 if streamPtr != nil {
240+ _ = streamOps.DisconnectStream(streamPtr) // Best-effort cleanup
238241 streamOps.DestroyStream(streamPtr)
239242 }
240243
@@ -220,6 +220,8 @@ func (p *player) deactivateStream() error {
220 220
221 // teardown releases player-owned stream and main-loop resources through221 // teardown releases player-owned stream and main-loop resources through
222 // StreamOps. Safe to call when fields are nil or on repeated invocation.222 // StreamOps. Safe to call when fields are nil or on repeated invocation.
223+// DisconnectStream is called before DestroyStream as best-effort cleanup;
224+// if disconnect fails, teardown continues (destroy is the definitive release).
223 func (p *player) teardown() {225 func (p *player) teardown() {
224 p.mu.Lock()226 p.mu.Lock()
225 streamOps := p.streamOps227 streamOps := p.streamOps
@@ -233,8 +235,9 @@ func (p *player) teardown() {
233 return235 return
234 }236 }
235 237
236- // Destroy stream first (it depends on the loop).238+ // Disconnect then destroy stream (disconnect is best-effort, destroy is definitive).
237 if streamPtr != nil {239 if streamPtr != nil {
240+ _ = streamOps.DisconnectStream(streamPtr) // Best-effort cleanup
238 streamOps.DestroyStream(streamPtr)241 streamOps.DestroyStream(streamPtr)
239 }242 }
240 243
M internal/core/player_test.go +55 -2
@@ -247,14 +247,15 @@ func TestPlayerStopWithoutStreamOpsIsNoop(t *testing.T) {
247247 }
248248
249249 // TestPlayerCloseTeardownCallsStreamOps verifies that Close() calls
250-// DestroyStream, QuitMainLoop, and DestroyMainLoop through StreamOps
250+// DisconnectStream, DestroyStream, QuitMainLoop, and DestroyMainLoop through StreamOps
251251 // when the corresponding pointers are set.
252252 func TestPlayerCloseTeardownCallsStreamOps(t *testing.T) {
253253 mockOps := mocks.NewMockStreamOps(t)
254254 fakeStream := unsafe.Pointer(uintptr(0xAAAA))
255255 fakeLoop := unsafe.Pointer(uintptr(0xBBBB))
256256
257- // Expect teardown calls in order: DestroyStream, QuitMainLoop, DestroyMainLoop.
257+ // Expect teardown calls in order: DisconnectStream, DestroyStream, QuitMainLoop, DestroyMainLoop.
258+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
258259 mockOps.EXPECT().DestroyStream(fakeStream).Return()
259260 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
260261 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
@@ -306,6 +307,7 @@ func TestPlayerRepeatedCloseIsSafe(t *testing.T) {
306307 fakeLoop := unsafe.Pointer(uintptr(0xDDDD))
307308
308309 // Only expect one set of teardown calls.
310+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
309311 mockOps.EXPECT().DestroyStream(fakeStream).Return()
310312 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
311313 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
@@ -332,6 +334,7 @@ func TestPlayerTeardownWithOnlyStreamPtr(t *testing.T) {
332334 mockOps := mocks.NewMockStreamOps(t)
333335 fakeStream := unsafe.Pointer(uintptr(0xEEEE))
334336
337+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
335338 mockOps.EXPECT().DestroyStream(fakeStream).Return()
336339
337340 p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
@@ -368,3 +371,53 @@ func TestPlayerTeardownWithOnlyLoopPtr(t *testing.T) {
368371 t.Error("expected loopPtr to be nil after teardown")
369372 }
370373 }
374+
375+// TestPlayerTeardownCallsDisconnectStream verifies that teardown calls
376+// DisconnectStream before DestroyStream when a stream exists.
377+func TestPlayerTeardownCallsDisconnectStream(t *testing.T) {
378+ mockOps := mocks.NewMockStreamOps(t)
379+ fakeStream := unsafe.Pointer(uintptr(0xAAAA))
380+ fakeLoop := unsafe.Pointer(uintptr(0xBBBB))
381+
382+ // Expect disconnect before destroy
383+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
384+ mockOps.EXPECT().DestroyStream(fakeStream).Return()
385+ mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
386+ mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
387+
388+ p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
389+ p.streamOps = mockOps
390+ p.streamPtr = fakeStream
391+ p.loopPtr = fakeLoop
392+ p.setState(PlayerStatePlaying)
393+
394+ if err := p.Close(); err != nil {
395+ t.Fatalf("Close failed: %v", err)
396+ }
397+}
398+
399+// TestPlayerTeardownContinuesOnDisconnectError verifies that teardown
400+// continues cleanup even if DisconnectStream returns an error.
401+func TestPlayerTeardownContinuesOnDisconnectError(t *testing.T) {
402+ mockOps := mocks.NewMockStreamOps(t)
403+ fakeStream := unsafe.Pointer(uintptr(0xCCCC))
404+ fakeLoop := unsafe.Pointer(uintptr(0xDDDD))
405+ disconnectErr := errors.New("disconnect failed")
406+
407+ // Disconnect fails, but destroy should still be called
408+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(disconnectErr)
409+ mockOps.EXPECT().DestroyStream(fakeStream).Return()
410+ mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
411+ mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
412+
413+ p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
414+ p.streamOps = mockOps
415+ p.streamPtr = fakeStream
416+ p.loopPtr = fakeLoop
417+ p.setState(PlayerStatePlaying)
418+
419+ // Close should succeed even though disconnect failed (best-effort cleanup)
420+ if err := p.Close(); err != nil {
421+ t.Fatalf("Close should succeed despite disconnect error: %v", err)
422+ }
423+}
@@ -247,14 +247,15 @@ func TestPlayerStopWithoutStreamOpsIsNoop(t *testing.T) {
247 }247 }
248 248
249 // TestPlayerCloseTeardownCallsStreamOps verifies that Close() calls249 // TestPlayerCloseTeardownCallsStreamOps verifies that Close() calls
250-// DestroyStream, QuitMainLoop, and DestroyMainLoop through StreamOps250+// DisconnectStream, DestroyStream, QuitMainLoop, and DestroyMainLoop through StreamOps
251 // when the corresponding pointers are set.251 // when the corresponding pointers are set.
252 func TestPlayerCloseTeardownCallsStreamOps(t *testing.T) {252 func TestPlayerCloseTeardownCallsStreamOps(t *testing.T) {
253 mockOps := mocks.NewMockStreamOps(t)253 mockOps := mocks.NewMockStreamOps(t)
254 fakeStream := unsafe.Pointer(uintptr(0xAAAA))254 fakeStream := unsafe.Pointer(uintptr(0xAAAA))
255 fakeLoop := unsafe.Pointer(uintptr(0xBBBB))255 fakeLoop := unsafe.Pointer(uintptr(0xBBBB))
256 256
257- // Expect teardown calls in order: DestroyStream, QuitMainLoop, DestroyMainLoop.257+ // Expect teardown calls in order: DisconnectStream, DestroyStream, QuitMainLoop, DestroyMainLoop.
258+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
258 mockOps.EXPECT().DestroyStream(fakeStream).Return()259 mockOps.EXPECT().DestroyStream(fakeStream).Return()
259 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()260 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
260 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()261 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
@@ -306,6 +307,7 @@ func TestPlayerRepeatedCloseIsSafe(t *testing.T) {
306 fakeLoop := unsafe.Pointer(uintptr(0xDDDD))307 fakeLoop := unsafe.Pointer(uintptr(0xDDDD))
307 308
308 // Only expect one set of teardown calls.309 // Only expect one set of teardown calls.
310+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
309 mockOps.EXPECT().DestroyStream(fakeStream).Return()311 mockOps.EXPECT().DestroyStream(fakeStream).Return()
310 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()312 mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
311 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()313 mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
@@ -332,6 +334,7 @@ func TestPlayerTeardownWithOnlyStreamPtr(t *testing.T) {
332 mockOps := mocks.NewMockStreamOps(t)334 mockOps := mocks.NewMockStreamOps(t)
333 fakeStream := unsafe.Pointer(uintptr(0xEEEE))335 fakeStream := unsafe.Pointer(uintptr(0xEEEE))
334 336
337+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
335 mockOps.EXPECT().DestroyStream(fakeStream).Return()338 mockOps.EXPECT().DestroyStream(fakeStream).Return()
336 339
337 p := newPlayer(PlayerConfig{}, PlayerCallbacks{})340 p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
@@ -368,3 +371,53 @@ func TestPlayerTeardownWithOnlyLoopPtr(t *testing.T) {
368 t.Error("expected loopPtr to be nil after teardown")371 t.Error("expected loopPtr to be nil after teardown")
369 }372 }
370 }373 }
374+
375+// TestPlayerTeardownCallsDisconnectStream verifies that teardown calls
376+// DisconnectStream before DestroyStream when a stream exists.
377+func TestPlayerTeardownCallsDisconnectStream(t *testing.T) {
378+ mockOps := mocks.NewMockStreamOps(t)
379+ fakeStream := unsafe.Pointer(uintptr(0xAAAA))
380+ fakeLoop := unsafe.Pointer(uintptr(0xBBBB))
381+
382+ // Expect disconnect before destroy
383+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(nil)
384+ mockOps.EXPECT().DestroyStream(fakeStream).Return()
385+ mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
386+ mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
387+
388+ p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
389+ p.streamOps = mockOps
390+ p.streamPtr = fakeStream
391+ p.loopPtr = fakeLoop
392+ p.setState(PlayerStatePlaying)
393+
394+ if err := p.Close(); err != nil {
395+ t.Fatalf("Close failed: %v", err)
396+ }
397+}
398+
399+// TestPlayerTeardownContinuesOnDisconnectError verifies that teardown
400+// continues cleanup even if DisconnectStream returns an error.
401+func TestPlayerTeardownContinuesOnDisconnectError(t *testing.T) {
402+ mockOps := mocks.NewMockStreamOps(t)
403+ fakeStream := unsafe.Pointer(uintptr(0xCCCC))
404+ fakeLoop := unsafe.Pointer(uintptr(0xDDDD))
405+ disconnectErr := errors.New("disconnect failed")
406+
407+ // Disconnect fails, but destroy should still be called
408+ mockOps.EXPECT().DisconnectStream(fakeStream).Return(disconnectErr)
409+ mockOps.EXPECT().DestroyStream(fakeStream).Return()
410+ mockOps.EXPECT().QuitMainLoop(fakeLoop).Return()
411+ mockOps.EXPECT().DestroyMainLoop(fakeLoop).Return()
412+
413+ p := newPlayer(PlayerConfig{}, PlayerCallbacks{})
414+ p.streamOps = mockOps
415+ p.streamPtr = fakeStream
416+ p.loopPtr = fakeLoop
417+ p.setState(PlayerStatePlaying)
418+
419+ // Close should succeed even though disconnect failed (best-effort cleanup)
420+ if err := p.Close(); err != nil {
421+ t.Fatalf("Close should succeed despite disconnect error: %v", err)
422+ }
423+}
M internal/ports/out/mocks/mock_StreamOps.go +18 -7
@@ -345,9 +345,20 @@ func (_c *MockStreamOps_DestroyStream_Call) RunAndReturn(run func(streamPtr unsa
345345 }
346346
347347 // DisconnectStream provides a mock function for the type MockStreamOps
348-func (_mock *MockStreamOps) DisconnectStream(streamPtr unsafe.Pointer) {
349- _mock.Called(streamPtr)
350- return
348+func (_mock *MockStreamOps) DisconnectStream(streamPtr unsafe.Pointer) error {
349+ ret := _mock.Called(streamPtr)
350+
351+ if len(ret) == 0 {
352+ panic("no return value specified for DisconnectStream")
353+ }
354+
355+ var r0 error
356+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) error); ok {
357+ r0 = returnFunc(streamPtr)
358+ } else {
359+ r0 = ret.Error(0)
360+ }
361+ return r0
351362 }
352363
353364 // MockStreamOps_DisconnectStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectStream'
@@ -374,13 +385,13 @@ func (_c *MockStreamOps_DisconnectStream_Call) Run(run func(streamPtr unsafe.Poi
374385 return _c
375386 }
376387
377-func (_c *MockStreamOps_DisconnectStream_Call) Return() *MockStreamOps_DisconnectStream_Call {
378- _c.Call.Return()
388+func (_c *MockStreamOps_DisconnectStream_Call) Return(err error) *MockStreamOps_DisconnectStream_Call {
389+ _c.Call.Return(err)
379390 return _c
380391 }
381392
382-func (_c *MockStreamOps_DisconnectStream_Call) RunAndReturn(run func(streamPtr unsafe.Pointer)) *MockStreamOps_DisconnectStream_Call {
383- _c.Run(run)
393+func (_c *MockStreamOps_DisconnectStream_Call) RunAndReturn(run func(streamPtr unsafe.Pointer) error) *MockStreamOps_DisconnectStream_Call {
394+ _c.Call.Return(run)
384395 return _c
385396 }
386397
@@ -345,9 +345,20 @@ func (_c *MockStreamOps_DestroyStream_Call) RunAndReturn(run func(streamPtr unsa
345 }345 }
346 346
347 // DisconnectStream provides a mock function for the type MockStreamOps347 // DisconnectStream provides a mock function for the type MockStreamOps
348-func (_mock *MockStreamOps) DisconnectStream(streamPtr unsafe.Pointer) {348+func (_mock *MockStreamOps) DisconnectStream(streamPtr unsafe.Pointer) error {
349- _mock.Called(streamPtr)349+ ret := _mock.Called(streamPtr)
350- return350+
351+ if len(ret) == 0 {
352+ panic("no return value specified for DisconnectStream")
353+ }
354+
355+ var r0 error
356+ if returnFunc, ok := ret.Get(0).(func(unsafe.Pointer) error); ok {
357+ r0 = returnFunc(streamPtr)
358+ } else {
359+ r0 = ret.Error(0)
360+ }
361+ return r0
351 }362 }
352 363
353 // MockStreamOps_DisconnectStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectStream'364 // MockStreamOps_DisconnectStream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectStream'
@@ -374,13 +385,13 @@ func (_c *MockStreamOps_DisconnectStream_Call) Run(run func(streamPtr unsafe.Poi
374 return _c385 return _c
375 }386 }
376 387
377-func (_c *MockStreamOps_DisconnectStream_Call) Return() *MockStreamOps_DisconnectStream_Call {388+func (_c *MockStreamOps_DisconnectStream_Call) Return(err error) *MockStreamOps_DisconnectStream_Call {
378- _c.Call.Return()389+ _c.Call.Return(err)
379 return _c390 return _c
380 }391 }
381 392
382-func (_c *MockStreamOps_DisconnectStream_Call) RunAndReturn(run func(streamPtr unsafe.Pointer)) *MockStreamOps_DisconnectStream_Call {393+func (_c *MockStreamOps_DisconnectStream_Call) RunAndReturn(run func(streamPtr unsafe.Pointer) error) *MockStreamOps_DisconnectStream_Call {
383- _c.Run(run)394+ _c.Call.Return(run)
384 return _c395 return _c
385 }396 }
386 397
M internal/ports/out/stream_ops.go +2 -1
@@ -25,7 +25,8 @@ type StreamOps interface {
2525 QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
2626
2727 // DisconnectStream disconnects the stream.
28- DisconnectStream(streamPtr unsafe.Pointer)
28+ // Returns an error if the operation fails.
29+ DisconnectStream(streamPtr unsafe.Pointer) error
2930
3031 // DestroyStream destroys the stream and frees its resources.
3132 DestroyStream(streamPtr unsafe.Pointer)
@@ -25,7 +25,8 @@ type StreamOps interface {
25 QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error25 QueueBuffer(streamPtr unsafe.Pointer, bufPtr unsafe.Pointer) error
26 26
27 // DisconnectStream disconnects the stream.27 // DisconnectStream disconnects the stream.
28- DisconnectStream(streamPtr unsafe.Pointer)28+ // Returns an error if the operation fails.
29+ DisconnectStream(streamPtr unsafe.Pointer) error
29 30
30 // DestroyStream destroys the stream and frees its resources.31 // DestroyStream destroys the stream and frees its resources.
31 DestroyStream(streamPtr unsafe.Pointer)32 DestroyStream(streamPtr unsafe.Pointer)

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