v0.2.0 4c97002

stream_params_test.go RawHistoryFolder

1package capi
2
3import (
4	"encoding/binary"
5	"errors"
6	"testing"
7	"unsafe"
8
9	portout "github.com/bnema/purego-pipewire/internal/ports/out"
10)
11
12// TestBuildRawAudioParamsProducesOneConnectParam verifies that buildRawAudioParams
13// produces exactly one connect param with the correct SPA_PARAM_EnumFormat structure
14// for a valid PlaybackFormat with planar float32.
15func TestBuildRawAudioParamsProducesOneConnectParam(t *testing.T) {
16	fmt := portout.PlaybackFormat{
17		SampleRate:      48000,
18		Channels:        2,
19		FramesPerBuffer: 1024,
20	}
21
22	cp, err := buildRawAudioParams(fmt)
23	if err != nil {
24		t.Fatalf("unexpected error: %v", err)
25	}
26
27	if cp.Count() != 1 {
28		t.Fatalf("expected 1 param, got %d", cp.Count())
29	}
30
31	// The param must be a non-nil pointer.
32	ptr := cp.Pointer()
33	if ptr == nil {
34		t.Fatal("param pointer must not be nil")
35	}
36
37	// Read the POD header from storage to verify structure.
38	// An SPA POD Object starts with: {uint32 size, uint32 type}
39	// type must be SPA_TYPE_Object (15).
40	podSize := binary.LittleEndian.Uint32(cp.storage[0:4])
41	podType := binary.LittleEndian.Uint32(cp.storage[4:8])
42
43	const spaTypeObject uint32 = 15
44	if podType != spaTypeObject {
45		t.Fatalf("expected POD type SPA_TYPE_Object (%d), got %d", spaTypeObject, podType)
46	}
47
48	// Immediately after the pod header is object body: {uint32 objType, uint32 id}
49	objType := binary.LittleEndian.Uint32(cp.storage[8:12])
50	objID := binary.LittleEndian.Uint32(cp.storage[12:16])
51
52	const wantSpaTypeObjectFormat uint32 = 0x40003
53	const spaParamEnumFormat uint32 = 3
54	if objType != wantSpaTypeObjectFormat {
55		t.Fatalf("expected object type SPA_TYPE_OBJECT_Format (0x%x), got 0x%x", wantSpaTypeObjectFormat, objType)
56	}
57	if objID != spaParamEnumFormat {
58		t.Fatalf("expected object id SPA_PARAM_EnumFormat (%d), got %d", spaParamEnumFormat, objID)
59	}
60
61	// The POD body size must cover all properties.
62	// Object body (8 bytes) + 5 properties × 24 bytes = 128 bytes
63	expectedBodySize := uint32(8 + 5*24)
64	if podSize != expectedBodySize {
65		t.Errorf("expected POD body size %d, got %d", expectedBodySize, podSize)
66	}
67
68	// Verify the first property: SPA_FORMAT_mediaType → Id(SPA_MEDIA_TYPE_audio)
69	dissectProperty(t, cp.storage[16:40], "mediaType",
70		spaFormatMediaType, spaTypeId, uint32(spaMediaTypeAudio))
71
72	// Verify the second property: SPA_FORMAT_mediaSubtype → Id(SPA_MEDIA_SUBTYPE_raw)
73	dissectProperty(t, cp.storage[40:64], "mediaSubtype",
74		spaFormatMediaSubtype, spaTypeId, uint32(spaMediaSubtypeRaw))
75
76	// Verify the third property: SPA_FORMAT_AUDIO_format → Id(SPA_AUDIO_FORMAT_F32P)
77	dissectProperty(t, cp.storage[64:88], "audio.format",
78		spaFormatAudioFormat, spaTypeId, spaAudioFormatF32P)
79
80	// Verify the fourth property: SPA_FORMAT_AUDIO_rate → Int(sample_rate)
81	dissectIntProperty(t, cp.storage[88:112], "audio.rate",
82		spaFormatAudioRate, 48000)
83
84	// Verify the fifth property: SPA_FORMAT_AUDIO_channels → Int(channels)
85	dissectIntProperty(t, cp.storage[112:136], "audio.channels",
86		spaFormatAudioChannels, 2)
87}
88
89// TestBuildRawAudioParamsRejectsInvalidFormat verifies that buildRawAudioParams
90// rejects zero or negative format values.
91func TestBuildRawAudioParamsRejectsInvalidFormat(t *testing.T) {
92	tests := []struct {
93		name   string
94		fmt    portout.PlaybackFormat
95		errMsg string
96	}{
97		{
98			name:   "zero_sample_rate",
99			fmt:    portout.PlaybackFormat{SampleRate: 0, Channels: 2, FramesPerBuffer: 1024},
100			errMsg: "sample rate",
101		},
102		{
103			name:   "negative_sample_rate",
104			fmt:    portout.PlaybackFormat{SampleRate: -1, Channels: 2, FramesPerBuffer: 1024},
105			errMsg: "sample rate",
106		},
107		{
108			name:   "zero_channels",
109			fmt:    portout.PlaybackFormat{SampleRate: 48000, Channels: 0, FramesPerBuffer: 1024},
110			errMsg: "channels",
111		},
112		{
113			name:   "negative_channels",
114			fmt:    portout.PlaybackFormat{SampleRate: 48000, Channels: -1, FramesPerBuffer: 1024},
115			errMsg: "channels",
116		},
117		{
118			name:   "zero_frames_per_buffer",
119			fmt:    portout.PlaybackFormat{SampleRate: 48000, Channels: 2, FramesPerBuffer: 0},
120			errMsg: "frames per buffer",
121		},
122		{
123			name:   "negative_frames_per_buffer",
124			fmt:    portout.PlaybackFormat{SampleRate: 48000, Channels: 2, FramesPerBuffer: -1},
125			errMsg: "frames per buffer",
126		},
127	}
128
129	for _, tt := range tests {
130		t.Run(tt.name, func(t *testing.T) {
131			_, err := buildRawAudioParams(tt.fmt)
132			if err == nil {
133				t.Fatalf("expected error for %s, got nil", tt.name)
134			}
135			if !errors.Is(err, ErrInvalidPlaybackFormat) {
136				t.Errorf("expected error wrapping ErrInvalidPlaybackFormat, got: %v", err)
137			}
138		})
139	}
140}
141
142// TestBuildRawAudioParamsDifferentRates verifies the builder encodes different
143// sample rates and channel counts correctly.
144func TestBuildRawAudioParamsDifferentRates(t *testing.T) {
145	fmt := portout.PlaybackFormat{
146		SampleRate:      44100,
147		Channels:        1,
148		FramesPerBuffer: 512,
149	}
150
151	cp, err := buildRawAudioParams(fmt)
152	if err != nil {
153		t.Fatalf("unexpected error: %v", err)
154	}
155
156	// The fourth property should be Int(44100)
157	dissectIntProperty(t, cp.storage[88:112], "audio.rate",
158		spaFormatAudioRate, 44100)
159
160	// The fifth property should be Int(1)
161	dissectIntProperty(t, cp.storage[112:136], "audio.channels",
162		spaFormatAudioChannels, 1)
163}
164
165// TestConnectParamsHelpers verifies the connectParams helper methods.
166func TestConnectParamsHelpers(t *testing.T) {
167	fmt := portout.PlaybackFormat{
168		SampleRate:      48000,
169		Channels:        2,
170		FramesPerBuffer: 1024,
171	}
172
173	cp, err := buildRawAudioParams(fmt)
174	if err != nil {
175		t.Fatalf("unexpected error: %v", err)
176	}
177
178	if cp.Count() != 1 {
179		t.Errorf("Count() = %d, want 1", cp.Count())
180	}
181
182	ptr := cp.Pointer()
183	if ptr == nil {
184		t.Error("Pointer() returned nil")
185	}
186
187	// Pointer() must return the address of the params slice entry (spa_pod **),
188	// NOT the raw spa_pod * that the entry points to.
189	expectedParamsAddr := unsafe.Pointer(&cp.params[0])
190	if ptr != expectedParamsAddr {
191		t.Errorf("Pointer() = %v, want &params[0] = %v", ptr, expectedParamsAddr)
192	}
193
194	// Pointer() must NOT equal the storage start (that would be spa_pod *,
195	// which is the wrong indirection level for pw_stream_connect).
196	storageAddr := unsafe.Pointer(&cp.storage[0])
197	if ptr == storageAddr {
198		t.Errorf("Pointer() must not equal &storage[0] (%v); it should be one indirection level above", storageAddr)
199	}
200}
201
202// dissectProperty checks an Id-valued property at the given byte slice.
203func dissectProperty(t *testing.T, prop []byte, label string, wantKey uint32, wantType uint32, wantValue uint32) {
204	t.Helper()
205	key := binary.LittleEndian.Uint32(prop[0:4])
206	flags := binary.LittleEndian.Uint32(prop[4:8])
207	valPodSize := binary.LittleEndian.Uint32(prop[8:12])
208	valPodType := binary.LittleEndian.Uint32(prop[12:16])
209	val := binary.LittleEndian.Uint32(prop[16:20])
210
211	if key != wantKey {
212		t.Errorf("%s: key = 0x%x, want 0x%x", label, key, wantKey)
213	}
214	if flags != 0 {
215		t.Errorf("%s: flags = %d, want 0", label, flags)
216	}
217	if valPodSize != 4 {
218		t.Errorf("%s: value pod size = %d, want 4", label, valPodSize)
219	}
220	if valPodType != wantType {
221		t.Errorf("%s: value pod type = %d, want %d", label, valPodType, wantType)
222	}
223	if val != wantValue {
224		t.Errorf("%s: value = %d, want %d", label, val, wantValue)
225	}
226}
227
228// dissectIntProperty checks an Int-valued property at the given byte slice.
229func dissectIntProperty(t *testing.T, prop []byte, label string, wantKey uint32, wantValue int32) {
230	t.Helper()
231	key := binary.LittleEndian.Uint32(prop[0:4])
232	flags := binary.LittleEndian.Uint32(prop[4:8])
233	valPodSize := binary.LittleEndian.Uint32(prop[8:12])
234	valPodType := binary.LittleEndian.Uint32(prop[12:16])
235	val := int32(binary.LittleEndian.Uint32(prop[16:20]))
236
237	const spaTypeInt uint32 = 4
238	if key != wantKey {
239		t.Errorf("%s: key = 0x%x, want 0x%x", label, key, wantKey)
240	}
241	if flags != 0 {
242		t.Errorf("%s: flags = %d, want 0", label, flags)
243	}
244	if valPodSize != 4 {
245		t.Errorf("%s: value pod size = %d, want 4", label, valPodSize)
246	}
247	if valPodType != spaTypeInt {
248		t.Errorf("%s: value pod type = %d, want %d", label, valPodType, spaTypeInt)
249	}
250	if val != wantValue {
251		t.Errorf("%s: value = %d, want %d", label, val, wantValue)
252	}
253}

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