detached 4c97002

pod_test.go RawHistoryFolder

1package spa
2
3import (
4	"encoding/binary"
5	"encoding/hex"
6	"errors"
7	"testing"
8)
9
10// Golden vectors are the bytes produced by the SPA reference builder
11// (spa/pod/builder.h, libspa 0.2 / PipeWire 1.6.9) for the same parameters
12// via spa_pod_builder_add_object / SPA_POD_CHOICE_* on amd64.
13const (
14	goldenEnumFormatSingle = "b80000000f00000003000400030000000100000000000000040000000300000002000000000000000200000000000000040000000300000001000000000000000100020000000000040000000300000008000000000000000300020000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
15	goldenEnumFormatMulti  = "d00000000f000000030004000300000001000000000000000400000003000000020000000000000002000000000000000400000003000000010000000000000001000200000000001c000000130000000300000000000000040000000300000008000000080000000c000000000000000300020000000000080000000a00000080070000380400000400020000000000080000000b0000000000000001000000050002000000000028000000130000000100000000000000080000000b000000900000000100000001000000010000009000000001000000"
16	goldenBuffers          = "c00000000f000000040004000500000001000000000000001c0000001300000001000000000000000400000004000000080000000200000010000000000000000200000000000000040000000400000001000000000000000300000000000000040000000400000000b004000000000004000000000000000400000004000000000500000000000005000000000000000400000004000000100000000000000006000000000000001400000013000000040000000000000004000000040000000400000000000000"
17	goldenMetaHeader       = "380000000f0000000500040006000000010000000000000004000000030000000100000000000000020000000000000004000000040000002000000000000000"
18)
19
20func mustHex(t testing.TB, s string) []byte {
21	t.Helper()
22	b, err := hex.DecodeString(s)
23	if err != nil {
24		t.Fatal(err)
25	}
26	return b
27}
28
29func TestVideoEnumFormatGoldenSingle(t *testing.T) {
30	var dst [512]byte
31	got, err := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx)
32	if err != nil {
33		t.Fatal(err)
34	}
35	if want := mustHex(t, goldenEnumFormatSingle); hex.EncodeToString(got) != hex.EncodeToString(want) {
36		t.Fatalf("got  %x\nwant %x", got, want)
37	}
38}
39
40func TestVideoEnumFormatGoldenMulti(t *testing.T) {
41	var dst [512]byte
42	got, err := VideoEnumFormat(dst[:], 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA)
43	if err != nil {
44		t.Fatal(err)
45	}
46	if want := mustHex(t, goldenEnumFormatMulti); hex.EncodeToString(got) != hex.EncodeToString(want) {
47		t.Fatalf("got  %x\nwant %x", got, want)
48	}
49}
50
51func TestVideoBuffersGolden(t *testing.T) {
52	var dst [512]byte
53	got, err := VideoBuffers(dst[:], 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd)
54	if err != nil {
55		t.Fatal(err)
56	}
57	if want := mustHex(t, goldenBuffers); hex.EncodeToString(got) != hex.EncodeToString(want) {
58		t.Fatalf("got  %x\nwant %x", got, want)
59	}
60}
61
62// TestBuildersExactFit checks each builder accepts a destination exactly as
63// long as its pod and rejects one byte less.
64func TestBuildersExactFit(t *testing.T) {
65	builds := map[string]func(dst []byte) ([]byte, error){
66		"enum single": func(dst []byte) ([]byte, error) { return VideoEnumFormat(dst, 320, 240, 60, VideoFormatBGRx) },
67		"enum multi": func(dst []byte) ([]byte, error) {
68			return VideoEnumFormat(dst, 1920, 1080, 144, VideoFormatBGRx, VideoFormatBGRA)
69		},
70		"buffers": func(dst []byte) ([]byte, error) { return VideoBuffers(dst, 320*240*4, 320*4, 2, 8, 16, 1<<DataMemFd) },
71		"meta":    MetaHeader,
72	}
73	for name, build := range builds {
74		var big [512]byte
75		pod, err := build(big[:])
76		if err != nil {
77			t.Fatalf("%s: %v", name, err)
78		}
79		if _, err := build(make([]byte, len(pod))); err != nil {
80			t.Errorf("%s: exact %d-byte destination: %v", name, len(pod), err)
81		}
82		if _, err := build(make([]byte, len(pod)-1)); !errors.Is(err, ErrPodFull) {
83			t.Errorf("%s: short destination: %v, want ErrPodFull", name, err)
84		}
85	}
86}
87
88func TestVideoBuffersRejectsInt32Overflow(t *testing.T) {
89	var dst [512]byte
90	if _, err := VideoBuffers(dst[:], 1<<31, 1<<17, 2, 8, 16, 1<<DataMemFd); !errors.Is(err, ErrPodState) {
91		t.Fatalf("size 1<<31: %v, want ErrPodState", err)
92	}
93}
94
95func TestMetaHeaderGolden(t *testing.T) {
96	var dst [128]byte
97	got, err := MetaHeader(dst[:])
98	if err != nil {
99		t.Fatal(err)
100	}
101	if want := mustHex(t, goldenMetaHeader); hex.EncodeToString(got) != hex.EncodeToString(want) {
102		t.Fatalf("got  %x\nwant %x", got, want)
103	}
104}
105
106// formatPod builds a negotiated (non-choice) Format object for parser tests.
107func formatPod(t testing.TB, dst []byte, format VideoFormat, w, h uint32) []byte {
108	t.Helper()
109	b := NewPodBuilder(dst)
110	b.Object(TypeObjectFormat, ParamFormat)
111	b.Prop(FormatKeyMediaType)
112	b.Id(MediaTypeVideo)
113	b.Prop(FormatKeyMediaSubtype)
114	b.Id(MediaSubtypeRaw)
115	b.Prop(FormatKeyVideoFormat)
116	b.Id(uint32(format))
117	b.Prop(FormatKeyVideoSize)
118	b.Rectangle(w, h)
119	b.Prop(FormatKeyVideoFramerate)
120	b.Fraction(60, 1)
121	b.End()
122	pod, err := b.Bytes()
123	if err != nil {
124		t.Fatal(err)
125	}
126	return pod
127}
128
129// goldenNegotiatedFormat is a Format param exactly as PipeWire 1.6.9 delivers
130// it to param_changed for a 320x240 BGRx stream (captured from a live stream):
131// every value is a Choice of type None around one value.
132const goldenNegotiatedFormat = "f80000000f0000000300040004000000010000000000000014000000130000000000000000000000040000000300000002000000000000000200000000000000140000001300000000000000000000000400000003000000010000000000000001000200000000001400000013000000000000000000000004000000030000000800000000000000030002000000000018000000130000000000000000000000080000000a00000040010000f00000000400020000000000080000000b0000000000000001000000050002000000000028000000130000000000000000000000080000000b0000003c0000000100000001000000010000003c00000001000000"
133
134func TestParseVideoFormatChoiceNone(t *testing.T) {
135	f, w, h, ok := ParseVideoFormat(mustHex(t, goldenNegotiatedFormat))
136	if !ok || VideoFormat(f) != VideoFormatBGRx || w != 320 || h != 240 {
137		t.Fatalf("parse = %d %d %d %v", f, w, h, ok)
138	}
139	// Every truncation of the real pod is rejected without panicking.
140	pod := mustHex(t, goldenNegotiatedFormat)
141	for n := 0; n < len(pod); n++ {
142		if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
143			t.Fatalf("truncated to %d bytes accepted", n)
144		}
145	}
146}
147
148func TestParseVideoFormatRoundTrip(t *testing.T) {
149	var dst [256]byte
150	for _, tc := range []struct {
151		f    VideoFormat
152		w, h int
153	}{{VideoFormatBGRx, 320, 240}, {VideoFormatBGRA, 3840, 2160}} {
154		pod := formatPod(t, dst[:], tc.f, uint32(tc.w), uint32(tc.h))
155		f, w, h, ok := ParseVideoFormat(pod)
156		if !ok || VideoFormat(f) != tc.f || w != tc.w || h != tc.h {
157			t.Fatalf("parse = %d %d %d %v, want %d %d %d", f, w, h, ok, tc.f, tc.w, tc.h)
158		}
159	}
160}
161
162func TestParseVideoFormatRejectsMalformed(t *testing.T) {
163	var dst [256]byte
164	pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
165
166	// Every truncation must be rejected without panicking.
167	for n := 0; n < len(pod); n++ {
168		if _, _, _, ok := ParseVideoFormat(pod[:n]); ok {
169			t.Fatalf("truncated to %d bytes accepted", n)
170		}
171	}
172	// Non-object, wrong object type, oversized declared size.
173	bad := append([]byte(nil), pod...)
174	binary.LittleEndian.PutUint32(bad[4:], TypeInt)
175	if _, _, _, ok := ParseVideoFormat(bad); ok {
176		t.Fatal("non-object accepted")
177	}
178	bad = append([]byte(nil), pod...)
179	binary.LittleEndian.PutUint32(bad[8:], TypeObjectParamBuffers)
180	if _, _, _, ok := ParseVideoFormat(bad); ok {
181		t.Fatal("wrong object type accepted")
182	}
183	bad = append([]byte(nil), pod...)
184	binary.LittleEndian.PutUint32(bad[0:], 1<<30)
185	if _, _, _, ok := ParseVideoFormat(bad); ok {
186		t.Fatal("oversized pod accepted")
187	}
188	// Property value size overflowing the object.
189	bad = append([]byte(nil), pod...)
190	binary.LittleEndian.PutUint32(bad[16+8:], 1<<31)
191	if _, _, _, ok := ParseVideoFormat(bad); ok {
192		t.Fatal("oversized property accepted")
193	}
194	// Zero and absurd sizes.
195	if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 0, 240)); ok {
196		t.Fatal("zero width accepted")
197	}
198	if _, _, _, ok := ParseVideoFormat(formatPod(t, dst[:], VideoFormatBGRx, 1<<24, 240)); ok {
199		t.Fatal("huge width accepted")
200	}
201	// An EnumFormat with a choice for the format is not a negotiated format.
202	var big [512]byte
203	enum, err := VideoEnumFormat(big[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
204	if err != nil {
205		t.Fatal(err)
206	}
207	if _, _, _, ok := ParseVideoFormat(enum); ok {
208		t.Fatal("choice format accepted")
209	}
210}
211
212func TestPodBuilderErrors(t *testing.T) {
213	// Buffer too small: error, no panic, for every size up to the full pod.
214	var full [512]byte
215	pod, err := VideoEnumFormat(full[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
216	if err != nil {
217		t.Fatal(err)
218	}
219	for n := 0; n < len(pod); n++ {
220		small := make([]byte, n)
221		if _, err := VideoEnumFormat(small, 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA); err != ErrPodFull {
222			t.Fatalf("size %d: err = %v, want ErrPodFull", n, err)
223		}
224	}
225	if _, err := VideoBuffers(make([]byte, 16), 1, 1, 2, 8, 16, 4); err != ErrPodFull {
226		t.Fatalf("buffers small: %v", err)
227	}
228	if _, err := MetaHeader(make([]byte, 8)); err != ErrPodFull {
229		t.Fatalf("meta small: %v", err)
230	}
231	// Invalid arguments.
232	if _, err := VideoEnumFormat(full[:], 0, 240, 60, VideoFormatBGRx); err == nil {
233		t.Fatal("zero width accepted")
234	}
235	if _, err := VideoEnumFormat(full[:], 320, 240, 60); err == nil {
236		t.Fatal("no formats accepted")
237	}
238	if _, err := VideoBuffers(full[:], 0, 4, 2, 8, 16, 4); err == nil {
239		t.Fatal("zero size accepted")
240	}
241	// Unbalanced builder use.
242	b := NewPodBuilder(full[:])
243	b.Prop(1)
244	if _, err := b.Bytes(); err != ErrPodState {
245		t.Fatalf("prop outside object: %v", err)
246	}
247	b = NewPodBuilder(full[:])
248	b.Object(TypeObjectFormat, ParamFormat)
249	if _, err := b.Bytes(); err != ErrPodState {
250		t.Fatalf("open object: %v", err)
251	}
252	b = NewPodBuilder(full[:])
253	b.End()
254	if _, err := b.Bytes(); err != ErrPodState {
255		t.Fatalf("end without object: %v", err)
256	}
257}
258
259func TestPodAllocations(t *testing.T) {
260	var dst [512]byte
261	pod := append([]byte(nil), formatPod(t, dst[:], VideoFormatBGRx, 320, 240)...)
262	real := mustHex(t, goldenNegotiatedFormat)
263	allocs := testing.AllocsPerRun(200, func() {
264		e, _ := VideoEnumFormat(dst[:], 320, 240, 60, VideoFormatBGRx, VideoFormatBGRA)
265		_, _ = VideoBuffers(dst[:], 307200, 1280, 2, 8, 16, 1<<DataMemFd)
266		_, _ = MetaHeader(dst[:])
267		_ = e
268		_, _, _, _ = ParseVideoFormat(pod)
269		_, _, _, _ = ParseVideoFormat(real)
270	})
271	if allocs != 0 {
272		t.Fatalf("allocs = %v, want 0", allocs)
273	}
274}

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