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