v0.1.0 3373d7c

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

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