1package capi
2
3import (
4 "errors"
5 "reflect"
6 "strings"
7 "testing"
8 "unsafe"
9
10 portout "github.com/bnema/purego-pipewire/internal/ports/out"
11)
12
13func opaquePtr() unsafe.Pointer {
14 return unsafe.Pointer(new(byte))
15}
16
17// TestDestroyStreamCallsPwStreamDestroy verifies that DestroyStream delegates
18// to the generated pw_stream_destroy binding (not pw_stream_disconnect).
19func TestDestroyStreamCallsPwStreamDestroy(t *testing.T) {
20 // Save and restore the package-level function var.
21 origDestroy := pw_stream_destroy
22 t.Cleanup(func() { pw_stream_destroy = origDestroy })
23
24 var called bool
25 var receivedPtr unsafe.Pointer
26 pw_stream_destroy = func(stream unsafe.Pointer) {
27 called = true
28 receivedPtr = stream
29 }
30
31 ops := &streamOpsImpl{
32 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
33 }
34
35 fakePtr := opaquePtr()
36 ops.pinned[fakePtr] = &streamCallbackStorage{}
37
38 ops.DestroyStream(fakePtr)
39
40 if !called {
41 t.Fatal("expected pw_stream_destroy to be called")
42 }
43 if receivedPtr != fakePtr {
44 t.Fatalf("pw_stream_destroy received %v, want %v", receivedPtr, fakePtr)
45 }
46
47 // Verify internal bookkeeping: pinned entry should be removed.
48 if _, exists := ops.pinned[fakePtr]; exists {
49 t.Error("expected callback storage to be unpinned after destroy")
50 }
51}
52
53// TestPWStreamEventsUsesFunctionPointerABI verifies that the generated callback
54// fields use raw function-pointer storage, not Go func pointers.
55func TestPWStreamEventsUsesFunctionPointerABI(t *testing.T) {
56 field, ok := reflect.TypeOf(pw_stream_events{}).FieldByName("process")
57 if !ok {
58 t.Fatal("pw_stream_events.process field missing")
59 }
60 if field.Type.Kind() != reflect.Uintptr {
61 t.Fatalf("pw_stream_events.process has kind %s, want uintptr", field.Type.Kind())
62 }
63}
64
65// TestDestroyStreamDoubleDestroySkipsSecondCall verifies idempotency:
66// a second DestroyStream on the same pointer does NOT call pw_stream_destroy again.
67func TestDestroyStreamDoubleDestroySkipsSecondCall(t *testing.T) {
68 origDestroy := pw_stream_destroy
69 t.Cleanup(func() { pw_stream_destroy = origDestroy })
70
71 callCount := 0
72 pw_stream_destroy = func(_ unsafe.Pointer) {
73 callCount++
74 }
75
76 ops := &streamOpsImpl{
77 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
78 }
79
80 fakePtr := opaquePtr()
81 ops.pinned[fakePtr] = &streamCallbackStorage{}
82
83 ops.DestroyStream(fakePtr)
84 ops.DestroyStream(fakePtr)
85
86 if callCount != 1 {
87 t.Fatalf("pw_stream_destroy called %d times, want 1", callCount)
88 }
89}
90
91// TestDestroyStreamBookkeepingDoesNotLeak verifies that internal tracking
92// does not retain entries after cleanup across many create/destroy cycles.
93func TestDestroyStreamBookkeepingDoesNotLeak(t *testing.T) {
94 origDestroy := pw_stream_destroy
95 t.Cleanup(func() { pw_stream_destroy = origDestroy })
96
97 pw_stream_destroy = func(_ unsafe.Pointer) {}
98
99 ops := &streamOpsImpl{
100 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
101 }
102
103 const cycles = 100
104 tokens := make([]byte, cycles)
105 for i := range tokens {
106 ptr := unsafe.Pointer(&tokens[i])
107 ops.pinned[ptr] = &streamCallbackStorage{}
108 ops.DestroyStream(ptr)
109 }
110
111 // After destroying all streams, pinned map should be empty.
112 if len(ops.pinned) != 0 {
113 t.Fatalf("pinned map has %d entries after %d cycles; expected 0", len(ops.pinned), cycles)
114 }
115}
116
117// TestQuitMainLoopCallsPwMainLoopQuit verifies that QuitMainLoop delegates
118// to the generated pw_main_loop_quit binding.
119func TestQuitMainLoopCallsPwMainLoopQuit(t *testing.T) {
120 origQuit := pw_main_loop_quit
121 t.Cleanup(func() { pw_main_loop_quit = origQuit })
122
123 var called bool
124 var receivedPtr unsafe.Pointer
125 pw_main_loop_quit = func(loop unsafe.Pointer) int32 {
126 called = true
127 receivedPtr = loop
128 return 0
129 }
130
131 ops := &streamOpsImpl{}
132 fakeLoop := opaquePtr()
133
134 ops.QuitMainLoop(fakeLoop)
135
136 if !called {
137 t.Fatal("expected pw_main_loop_quit to be called")
138 }
139 if receivedPtr != fakeLoop {
140 t.Fatalf("pw_main_loop_quit received %v, want %v", receivedPtr, fakeLoop)
141 }
142}
143
144// TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn verifies that
145// ConnectPlaybackStream returns a PWError with function name and return code.
146func TestConnectPlaybackStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
147 origConnect := pw_stream_connect
148 t.Cleanup(func() { pw_stream_connect = origConnect })
149
150 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
151 return -22 // EINVAL
152 }
153
154 ops := &streamOpsImpl{}
155 fakePtr := opaquePtr()
156
157 validFmt := portout.PlaybackFormat{
158 SampleRate: 48000,
159 Channels: 2,
160 FramesPerBuffer: 1024,
161 }
162 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
163 if err == nil {
164 t.Fatal("expected error for negative return code, got nil")
165 }
166
167 var pwErr *PWError
168 if !errors.As(err, &pwErr) {
169 t.Fatalf("expected *PWError, got %T", err)
170 }
171 if pwErr.Func != "pw_stream_connect" {
172 t.Errorf("expected Func='pw_stream_connect', got '%s'", pwErr.Func)
173 }
174 if pwErr.Code != -22 {
175 t.Errorf("expected Code=-22, got %d", pwErr.Code)
176 }
177}
178
179// TestSetStreamActiveReturnsTypedErrorOnNegativeReturn verifies that
180// SetStreamActive returns a PWError with function name and return code.
181func TestSetStreamActiveReturnsTypedErrorOnNegativeReturn(t *testing.T) {
182 origSetActive := pw_stream_set_active
183 t.Cleanup(func() { pw_stream_set_active = origSetActive })
184
185 pw_stream_set_active = func(stream unsafe.Pointer, active bool) int32 {
186 return -9 // EBADF
187 }
188
189 ops := &streamOpsImpl{}
190 fakePtr := opaquePtr()
191
192 err := ops.SetStreamActive(fakePtr, true)
193 if err == nil {
194 t.Fatal("expected error for negative return code, got nil")
195 }
196
197 var pwErr *PWError
198 if !errors.As(err, &pwErr) {
199 t.Fatalf("expected *PWError, got %T", err)
200 }
201 if pwErr.Func != "pw_stream_set_active" {
202 t.Errorf("expected Func='pw_stream_set_active', got '%s'", pwErr.Func)
203 }
204 if pwErr.Code != -9 {
205 t.Errorf("expected Code=-9, got %d", pwErr.Code)
206 }
207}
208
209// TestQueueBufferReturnsTypedErrorOnNegativeReturn verifies that
210// QueueBuffer returns a PWError with function name and return code.
211func TestQueueBufferReturnsTypedErrorOnNegativeReturn(t *testing.T) {
212 origQueue := pw_stream_queue_buffer
213 t.Cleanup(func() { pw_stream_queue_buffer = origQueue })
214
215 pw_stream_queue_buffer = func(stream unsafe.Pointer, buffer unsafe.Pointer) int32 {
216 return -5 // EIO
217 }
218
219 ops := &streamOpsImpl{}
220 fakePtr := opaquePtr()
221 fakeBuf := opaquePtr()
222
223 err := ops.QueueBuffer(fakePtr, fakeBuf)
224 if err == nil {
225 t.Fatal("expected error for negative return code, got nil")
226 }
227
228 var pwErr *PWError
229 if !errors.As(err, &pwErr) {
230 t.Fatalf("expected *PWError, got %T", err)
231 }
232 if pwErr.Func != "pw_stream_queue_buffer" {
233 t.Errorf("expected Func='pw_stream_queue_buffer', got '%s'", pwErr.Func)
234 }
235 if pwErr.Code != -5 {
236 t.Errorf("expected Code=-5, got %d", pwErr.Code)
237 }
238}
239
240// TestDisconnectStreamReturnsTypedErrorOnNegativeReturn verifies that
241// DisconnectStream returns a PWError with function name and return code.
242func TestDisconnectStreamReturnsTypedErrorOnNegativeReturn(t *testing.T) {
243 origDisconnect := pw_stream_disconnect
244 t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
245
246 pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
247 return -19 // ENODEV
248 }
249
250 ops := &streamOpsImpl{}
251 fakePtr := opaquePtr()
252
253 err := ops.DisconnectStream(fakePtr)
254 if err == nil {
255 t.Fatal("expected error for negative return code, got nil")
256 }
257
258 var pwErr *PWError
259 if !errors.As(err, &pwErr) {
260 t.Fatalf("expected *PWError, got %T", err)
261 }
262 if pwErr.Func != "pw_stream_disconnect" {
263 t.Errorf("expected Func='pw_stream_disconnect', got '%s'", pwErr.Func)
264 }
265 if pwErr.Code != -19 {
266 t.Errorf("expected Code=-19, got %d", pwErr.Code)
267 }
268}
269
270// TestDisconnectStreamReturnsNilOnSuccess verifies that DisconnectStream
271// returns nil when the return code is non-negative.
272func TestDisconnectStreamReturnsNilOnSuccess(t *testing.T) {
273 origDisconnect := pw_stream_disconnect
274 t.Cleanup(func() { pw_stream_disconnect = origDisconnect })
275
276 pw_stream_disconnect = func(stream unsafe.Pointer) int32 {
277 return 0 // Success
278 }
279
280 ops := &streamOpsImpl{}
281 fakePtr := opaquePtr()
282
283 err := ops.DisconnectStream(fakePtr)
284 if err != nil {
285 t.Fatalf("expected nil on success, got %v", err)
286 }
287}
288
289// TestConnectPlaybackStreamDelegatesFramesPerBufferValidation verifies that
290// ConnectPlaybackStream rejects invalid FramesPerBuffer values via the helper
291// and does not reach pw_stream_connect.
292func TestConnectPlaybackStreamDelegatesFramesPerBufferValidation(t *testing.T) {
293 origConnect := pw_stream_connect
294 t.Cleanup(func() { pw_stream_connect = origConnect })
295
296 var called bool
297 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
298 called = true
299 return 0
300 }
301
302 ops := &streamOpsImpl{}
303 fakePtr := opaquePtr()
304
305 fmt := portout.PlaybackFormat{SampleRate: 48000, Channels: 2, FramesPerBuffer: 0}
306 err := ops.ConnectPlaybackStream(fakePtr, fmt)
307 if err == nil {
308 t.Fatal("expected error for missing FramesPerBuffer, got nil")
309 }
310 if !errors.Is(err, ErrInvalidPlaybackFormat) {
311 t.Fatalf("expected error wrapping ErrInvalidPlaybackFormat, got: %v", err)
312 }
313 if !strings.Contains(err.Error(), "frames per buffer") {
314 t.Fatalf("expected helper FramesPerBuffer validation, got: %v", err)
315 }
316 if called {
317 t.Fatal("pw_stream_connect should not be called for invalid FramesPerBuffer")
318 }
319}
320
321// TestConnectPlaybackStreamDelegatesSampleRateAndChannelsValidation verifies
322// that invalid sample rate and channel values are rejected by buildRawAudioParams.
323func TestConnectPlaybackStreamDelegatesSampleRateAndChannelsValidation(t *testing.T) {
324 origConnect := pw_stream_connect
325 t.Cleanup(func() { pw_stream_connect = origConnect })
326
327 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
328 t.Fatal("pw_stream_connect should not be called for invalid sample rate or channels")
329 return 0
330 }
331
332 ops := &streamOpsImpl{}
333 fakePtr := opaquePtr()
334
335 tests := []struct {
336 name string
337 fmt portout.PlaybackFormat
338 wantMsg string
339 }{
340 {
341 name: "zero_sample_rate",
342 fmt: portout.PlaybackFormat{SampleRate: 0, Channels: 2, FramesPerBuffer: 1024},
343 wantMsg: "sample rate must be positive",
344 },
345 {
346 name: "zero_channels",
347 fmt: portout.PlaybackFormat{SampleRate: 48000, Channels: 0, FramesPerBuffer: 1024},
348 wantMsg: "channels must be positive",
349 },
350 }
351
352 for _, tt := range tests {
353 t.Run(tt.name, func(t *testing.T) {
354 err := ops.ConnectPlaybackStream(fakePtr, tt.fmt)
355 if err == nil {
356 t.Fatalf("expected error for %s, got nil", tt.name)
357 }
358 if !errors.Is(err, ErrInvalidPlaybackFormat) {
359 t.Fatalf("expected error wrapping ErrInvalidPlaybackFormat, got: %v", err)
360 }
361 if !strings.Contains(err.Error(), tt.wantMsg) {
362 t.Fatalf("expected error to contain %q, got: %v", tt.wantMsg, err)
363 }
364 if strings.Contains(err.Error(), "FramesPerBuffer") {
365 t.Fatalf("expected helper validation to report only sample rate/channels, got: %v", err)
366 }
367 })
368 }
369}
370
371// TestConnectPlaybackStreamAcceptsValidFormat verifies that ConnectPlaybackStream
372// accepts a valid PlaybackFormat and calls pw_stream_connect successfully.
373// NOTE: The format is not yet forwarded to PipeWire as SPA params; this test
374// only confirms that the format is accepted at the validation seam. A subsequent
375// task will add tests verifying that the format values reach the C call.
376func TestConnectPlaybackStreamAcceptsValidFormat(t *testing.T) {
377 origConnect := pw_stream_connect
378 t.Cleanup(func() { pw_stream_connect = origConnect })
379
380 var called bool
381 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
382 called = true
383 return 0
384 }
385
386 ops := &streamOpsImpl{}
387 fakePtr := opaquePtr()
388
389 validFmt := portout.PlaybackFormat{
390 SampleRate: 48000,
391 Channels: 2,
392 FramesPerBuffer: 1024,
393 }
394 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
395 if err != nil {
396 t.Fatalf("expected nil error for valid format, got %v", err)
397 }
398 if !called {
399 t.Fatal("expected pw_stream_connect to be called")
400 }
401}
402
403// TestRunMainLoopReturnsPWErrorOnNegativeReturn verifies that RunMainLoop
404// returns a typed PWError when pw_main_loop_run returns a negative value.
405func TestRunMainLoopReturnsPWErrorOnNegativeResult(t *testing.T) {
406 origRun := pw_main_loop_run
407 t.Cleanup(func() { pw_main_loop_run = origRun })
408
409 pw_main_loop_run = func(loop unsafe.Pointer) int32 {
410 return -22 // EINVAL
411 }
412
413 ops := &streamOpsImpl{}
414 fakeLoop := opaquePtr()
415
416 err := ops.RunMainLoop(fakeLoop)
417 if err == nil {
418 t.Fatal("expected error for negative return code, got nil")
419 }
420
421 var pwErr *PWError
422 if !errors.As(err, &pwErr) {
423 t.Fatalf("expected *PWError, got %T", err)
424 }
425 if pwErr.Func != "pw_main_loop_run" {
426 t.Errorf("expected Func='pw_main_loop_run', got '%s'", pwErr.Func)
427 }
428 if pwErr.Code != -22 {
429 t.Errorf("expected Code=-22, got %d", pwErr.Code)
430 }
431}
432
433// TestRunMainLoopReturnsNilOnSuccess verifies that RunMainLoop
434// returns nil when pw_main_loop_run returns zero.
435func TestRunMainLoopReturnsNilOnSuccess(t *testing.T) {
436 origRun := pw_main_loop_run
437 t.Cleanup(func() { pw_main_loop_run = origRun })
438
439 pw_main_loop_run = func(loop unsafe.Pointer) int32 {
440 return 0
441 }
442
443 ops := &streamOpsImpl{}
444 fakeLoop := opaquePtr()
445
446 err := ops.RunMainLoop(fakeLoop)
447 if err != nil {
448 t.Fatalf("expected nil on success, got %v", err)
449 }
450}
451
452// TestCreatePlaybackStreamUsesPWLoopFromMainLoop verifies that CreatePlaybackStream
453// calls pw_main_loop_get_loop to obtain the pw_loop pointer and passes it to
454// pw_stream_new_simple (not the raw main loop pointer).
455func TestCreatePlaybackStreamUsesPWLoopFromMainLoop(t *testing.T) {
456 origGetLoop := pw_main_loop_get_loop
457 origNewSimple := pw_stream_new_simple
458 t.Cleanup(func() {
459 pw_main_loop_get_loop = origGetLoop
460 pw_stream_new_simple = origNewSimple
461 })
462
463 fakeMainLoop := opaquePtr()
464 fakePWLoop := opaquePtr()
465 var gotContext unsafe.Pointer
466
467 pw_main_loop_get_loop = func(loop unsafe.Pointer) unsafe.Pointer {
468 if loop != fakeMainLoop {
469 t.Errorf("pw_main_loop_get_loop received %v, want %v", loop, fakeMainLoop)
470 }
471 return fakePWLoop
472 }
473
474 pw_stream_new_simple = func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
475 gotContext = context
476 // Return a non-nil fake stream pointer.
477 return opaquePtr()
478 }
479
480 ops := &streamOpsImpl{
481 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
482 }
483
484 stream, err := ops.CreatePlaybackStream(fakeMainLoop, "test", func() {})
485 if err != nil {
486 t.Fatalf("unexpected error: %v", err)
487 }
488 if stream == nil {
489 t.Fatal("expected non-nil stream pointer")
490 }
491 if gotContext != fakePWLoop {
492 t.Errorf("pw_stream_new_simple received context %v, want %v (the pw_loop from pw_main_loop_get_loop)", gotContext, fakePWLoop)
493 }
494}
495
496// TestCreatePlaybackStreamReturnsPWErrorWhenPWLoopIsNull verifies that
497// CreatePlaybackStream returns a typed PWError when pw_main_loop_get_loop
498// returns nil.
499func TestCreatePlaybackStreamReturnsPWErrorWhenPWLoopIsNull(t *testing.T) {
500 origGetLoop := pw_main_loop_get_loop
501 origNewSimple := pw_stream_new_simple
502 t.Cleanup(func() {
503 pw_main_loop_get_loop = origGetLoop
504 pw_stream_new_simple = origNewSimple
505 })
506
507 pw_main_loop_get_loop = func(loop unsafe.Pointer) unsafe.Pointer {
508 return nil
509 }
510
511 // pw_stream_new_simple should NOT be called.
512 pw_stream_new_simple = func(context unsafe.Pointer, name *byte, props unsafe.Pointer, events unsafe.Pointer, data unsafe.Pointer) unsafe.Pointer {
513 t.Fatal("pw_stream_new_simple should not be called when pw_main_loop_get_loop returns nil")
514 return nil
515 }
516
517 ops := &streamOpsImpl{
518 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
519 }
520
521 fakeMainLoop := opaquePtr()
522 _, err := ops.CreatePlaybackStream(fakeMainLoop, "test", func() {})
523 if err == nil {
524 t.Fatal("expected error when pw_main_loop_get_loop returns nil")
525 }
526
527 var pwErr *PWError
528 if !errors.As(err, &pwErr) {
529 t.Fatalf("expected *PWError, got %T: %v", err, err)
530 }
531 if pwErr.Func != "pw_main_loop_get_loop" {
532 t.Errorf("expected Func='pw_main_loop_get_loop', got '%s'", pwErr.Func)
533 }
534}
535
536// TestConnectPlaybackStreamPassesParamsFromHelper verifies that ConnectPlaybackStream
537// calls buildRawAudioParams and passes the resulting non-nil params and nParams > 0
538// into pw_stream_connect.
539func TestConnectPlaybackStreamPassesParamsFromHelper(t *testing.T) {
540 origConnect := pw_stream_connect
541 t.Cleanup(func() { pw_stream_connect = origConnect })
542
543 var gotPorts unsafe.Pointer
544 var gotNPorts uint32
545
546 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
547 gotPorts = ports
548 gotNPorts = n_ports
549 return 0
550 }
551
552 ops := &streamOpsImpl{}
553 fakePtr := opaquePtr()
554
555 validFmt := portout.PlaybackFormat{
556 SampleRate: 48000,
557 Channels: 2,
558 FramesPerBuffer: 1024,
559 }
560 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
561 if err != nil {
562 t.Fatalf("expected nil error for valid format, got %v", err)
563 }
564
565 if gotPorts == nil {
566 t.Fatal("expected non-nil params pointer from buildRawAudioParams, got nil")
567 }
568 if gotNPorts == 0 {
569 t.Fatal("expected nParams > 0 from buildRawAudioParams, got 0")
570 }
571}
572
573// TestConnectPlaybackStreamPinsParamsAfterSuccess verifies that ConnectPlaybackStream
574// stores the connectParams in pinnedParams so the SPA POD backing storage outlives
575// the function call.
576func TestConnectPlaybackStreamPinsParamsAfterSuccess(t *testing.T) {
577 origConnect := pw_stream_connect
578 t.Cleanup(func() { pw_stream_connect = origConnect })
579
580 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
581 return 0
582 }
583
584 ops := &streamOpsImpl{}
585 fakePtr := opaquePtr()
586
587 validFmt := portout.PlaybackFormat{
588 SampleRate: 48000,
589 Channels: 2,
590 FramesPerBuffer: 1024,
591 }
592 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
593 if err != nil {
594 t.Fatalf("unexpected error: %v", err)
595 }
596
597 cp, ok := ops.pinnedParams[fakePtr]
598 if !ok {
599 t.Fatal("expected connectParams to be pinned for stream after ConnectPlaybackStream")
600 }
601 if cp == nil {
602 t.Fatal("pinned connectParams should not be nil")
603 }
604 // Verify the pinned params point to real SPA POD data.
605 if cp.Count() != 1 {
606 t.Fatalf("expected pinned params Count()=1, got %d", cp.Count())
607 }
608 if cp.Pointer() == nil {
609 t.Fatal("expected pinned params Pointer() to be non-nil")
610 }
611}
612
613// TestConnectPlaybackStreamDoesNotPinParamsOnFailure verifies that when pw_stream_connect
614// returns a negative result, connectParams are NOT pinned.
615func TestConnectPlaybackStreamDoesNotPinParamsOnFailure(t *testing.T) {
616 origConnect := pw_stream_connect
617 t.Cleanup(func() { pw_stream_connect = origConnect })
618
619 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
620 return -22 // EINVAL
621 }
622
623 ops := &streamOpsImpl{}
624 fakePtr := opaquePtr()
625
626 validFmt := portout.PlaybackFormat{
627 SampleRate: 48000,
628 Channels: 2,
629 FramesPerBuffer: 1024,
630 }
631 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
632 if err == nil {
633 t.Fatal("expected error for negative return code")
634 }
635
636 if _, ok := ops.pinnedParams[fakePtr]; ok {
637 t.Fatal("connectParams should NOT be pinned when pw_stream_connect fails")
638 }
639}
640
641// TestDestroyStreamReleasesPinnedParams verifies that DestroyStream removes both the
642// callback storage and the connect params from the pinned maps.
643func TestDestroyStreamReleasesPinnedParams(t *testing.T) {
644 origConnect := pw_stream_connect
645 origDestroy := pw_stream_destroy
646 t.Cleanup(func() {
647 pw_stream_connect = origConnect
648 pw_stream_destroy = origDestroy
649 })
650
651 pw_stream_connect = func(stream unsafe.Pointer, direction int32, id uint32, flags uint32, ports unsafe.Pointer, n_ports uint32) int32 {
652 return 0
653 }
654 pw_stream_destroy = func(_ unsafe.Pointer) {}
655
656 ops := &streamOpsImpl{
657 pinned: make(map[unsafe.Pointer]*streamCallbackStorage),
658 }
659
660 fakePtr := opaquePtr()
661 ops.pinned[fakePtr] = &streamCallbackStorage{}
662
663 validFmt := portout.PlaybackFormat{
664 SampleRate: 48000,
665 Channels: 2,
666 FramesPerBuffer: 1024,
667 }
668 err := ops.ConnectPlaybackStream(fakePtr, validFmt)
669 if err != nil {
670 t.Fatalf("unexpected error: %v", err)
671 }
672
673 // Verify params are pinned.
674 if _, ok := ops.pinnedParams[fakePtr]; !ok {
675 t.Fatal("expected connectParams to be pinned before DestroyStream")
676 }
677
678 ops.DestroyStream(fakePtr)
679
680 // Both maps should no longer contain the stream.
681 if _, ok := ops.pinned[fakePtr]; ok {
682 t.Error("expected callback storage to be removed after DestroyStream")
683 }
684 if _, ok := ops.pinnedParams[fakePtr]; ok {
685 t.Error("expected connectParams to be removed after DestroyStream")
686 }
687}