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 ¶ms[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}