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