v0.2.0 4c97002

stream_ops_test.go RawHistoryFolder

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}

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