v0.1.1 0e08fa2

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
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}

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