main 602baa7

capture_test.go RawHistoryFolder

1//go:build xserver
2
3package xtest
4
5import (
6	"bytes"
7	"encoding/binary"
8	"encoding/hex"
9	"encoding/json"
10	"flag"
11	"fmt"
12	"io"
13	"maps"
14	"net"
15	"os"
16	"os/exec"
17	"path/filepath"
18	"slices"
19	"strconv"
20	"strings"
21	"testing"
22	"time"
23
24	"golang.org/x/sys/unix"
25)
26
27var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them")
28
29// field is a server-dependent value inside a golden: the size-byte
30// little-endian integer at Off must equal the server's value for Role.
31type field struct {
32	Off, Size int
33	Role      string
34}
35
36// serverFields lists, per golden, every value the X server chooses: resource
37// IDs derived from a server-assigned base, the root window and its visual and
38// colormap, interned atoms, extension opcodes and bases, and timestamps.
39// Compare mode checks that the golden holds the value recorded at capture
40// (xvfb-roles.json) and the live packet holds the live server's value, then
41// masks both and compares the remaining bytes exactly. Role "time" is a
42// per-run server timestamp and is masked without a value check.
43var serverFields = map[string][]field{
44	"xproto/MapWindow.req":              {{4, 4, "root"}},
45	"xproto/GetGeometry.req":            {{4, 4, "root"}},
46	"xproto/ChangeProperty.req":         {{4, 4, "root"}, {8, 4, "atom"}},
47	"xproto/CreateWindow.req":           {{4, 4, "child"}, {8, 4, "root"}},
48	"xproto/ChangeWindowAttributes.req": {{4, 4, "child"}},
49	"xproto/ConfigureWindow.req":        {{4, 4, "child"}},
50	"xproto/ConfigureWindowSparse.req":  {{4, 4, "child"}},
51	"xproto/ConfigureWindowAll.req":     {{4, 4, "child"}, {32, 4, "sibling"}},
52	"render/CreatePicture.req":          {{0, 1, "renderMajor"}, {4, 4, "picture"}, {8, 4, "child"}, {12, 4, "rootFormat"}},
53	"res/QueryClientIds.req":            {{0, 1, "resMajor"}},
54	"xfixes/QueryVersion.req":           {{0, 1, "xfixesMajor"}},
55	"xproto/QueryExtension.reply":       {{9, 1, "xfixesMajor"}, {10, 1, "xfixesEvent"}, {11, 1, "xfixesError"}},
56	"xproto/GetGeometry.reply":          {{8, 4, "root"}},
57	"xproto/InternAtom.reply":           {{8, 4, "atom"}},
58	"xproto/GetWindowAttributes.reply":  {{8, 4, "rootVisual"}, {28, 4, "rootColormap"}},
59	"xproto/QueryTree.reply":            {{8, 4, "root"}, {32, 4, "sibling"}, {36, 4, "child"}},
60	"res/QueryClientIds.reply":          {{44, 4, "base"}},
61	"xproto/CreateNotify.event":         {{4, 4, "root"}, {8, 4, "client2"}},
62	"xproto/MapRequest.event":           {{4, 4, "root"}, {8, 4, "client2"}},
63	"xproto/ConfigureRequest.event":     {{4, 4, "root"}, {8, 4, "client2"}},
64	"xproto/MapNotify.event":            {{4, 4, "root"}, {8, 4, "client2"}},
65	"xproto/UnmapNotify.event":          {{4, 4, "root"}, {8, 4, "client2"}},
66	"xproto/DestroyNotify.event":        {{4, 4, "root"}, {8, 4, "client2"}},
67	"xproto/PropertyNotify.event":       {{4, 4, "root"}, {8, 4, "atom"}, {12, 4, "time"}},
68	"xproto/ClientMessage.event":        {{4, 4, "client2"}, {8, 4, "atom"}},
69	"xproto/SelectionNotify.event":      {{8, 4, "child"}},
70	"xfixes/SelectionNotify.event":      {{0, 1, "xfixesEvent"}, {4, 4, "root"}, {8, 4, "client2"}, {16, 4, "time"}, {20, 4, "time"}},
71	"xproto/BadWindow.error":            {{4, 4, "badWindow"}},
72	"xfixes/BadRegion.error":            {{1, 1, "xfixesError"}, {4, 4, "badRegion"}, {10, 1, "xfixesMajor"}},
73}
74
75// structural replies depend on the server build itself (extension list,
76// visuals and picture formats), so live compare checks invariants instead of
77// bytes; unit tests still decode the committed golden byte-exactly.
78var structural = map[string]func(p []byte, roles map[string]uint32) error{
79	"xproto/ListExtensions.reply":   checkListExtensions,
80	"render/QueryPictFormats.reply": checkQueryPictFormats,
81}
82
83const rolesPath = "../../testdata/golden/xvfb-roles.json"
84
85// xvfbArgs lets a run simulate a different server layout, e.g.
86// XTURBO_XVFB_ARGS="-extension GLX" shifts resource IDs, atoms and visuals.
87func xvfbArgs() []string {
88	args := []string{"-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac"}
89	return append(args, strings.Fields(os.Getenv("XTURBO_XVFB_ARGS"))...)
90}
91
92// TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By
93// default it compares every captured packet with the committed golden and
94// fails on a difference; -update rewrites the goldens instead.
95func TestCaptureRequests(t *testing.T) {
96	if _, err := exec.LookPath("Xvfb"); err != nil {
97		t.Skip("Xvfb not installed")
98	}
99	// displayfd avoids display-number races and tells us when Xvfb is ready.
100	r, w, err := os.Pipe()
101	if err != nil {
102		t.Fatal(err)
103	}
104	// Xvfb writes to displayfd again after a server reset; keep the read end
105	// open until the test ends so that write cannot kill the server.
106	t.Cleanup(func() { _ = r.Close() })
107	cmd := exec.Command("Xvfb", xvfbArgs()...)
108	cmd.ExtraFiles = []*os.File{w}
109	var log bytes.Buffer
110	cmd.Stderr = &log
111	if err = cmd.Start(); err != nil {
112		w.Close()
113		t.Fatal(err)
114	}
115	w.Close()
116	defer func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }()
117	_ = r.SetReadDeadline(time.Now().Add(5 * time.Second))
118	var display [32]byte
119	n, err := r.Read(display[:])
120	if err != nil {
121		t.Fatalf("Xvfb: %v: %s", err, log.String())
122	}
123	num, err := strconv.Atoi(strings.TrimSpace(string(display[:n])))
124	if err != nil {
125		t.Fatal(err)
126	}
127	server, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
128	if err != nil {
129		t.Fatal(err)
130	}
131	defer server.Close()
132	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
133	if err != nil {
134		t.Fatal(err)
135	}
136	a := os.NewFile(uintptr(fds[0]), "capture-client")
137	b := os.NewFile(uintptr(fds[1]), "capture-tee")
138	client, err := net.FileConn(a)
139	a.Close()
140	if err != nil {
141		b.Close()
142		t.Fatal(err)
143	}
144	defer client.Close()
145	tee, err := net.FileConn(b)
146	b.Close()
147	if err != nil {
148		t.Fatal(err)
149	}
150	defer tee.Close()
151	_ = client.SetDeadline(time.Now().Add(5 * time.Second))
152	_ = server.SetDeadline(time.Now().Add(5 * time.Second))
153	var recorded bytes.Buffer
154	copied := make(chan error, 1)
155	go func() { _, err := io.Copy(server, io.TeeReader(tee, &recorded)); copied <- err }()
156	go func() { _, _ = io.Copy(tee, server) }()
157	write := func(p []byte) {
158		t.Helper()
159		if _, err := client.Write(p); err != nil {
160			t.Fatal(err)
161		}
162	}
163	read := func(p []byte) {
164		t.Helper()
165		if _, err := io.ReadFull(client, p); err != nil {
166			t.Fatal(err)
167		}
168	}
169	// Independent reference encoder: no generated bindings participate in capture.
170	write([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
171	var setupHead [8]byte
172	read(setupHead[:])
173	if setupHead[0] != 1 {
174		t.Fatalf("setup failed: %x", setupHead)
175	}
176	setup := make([]byte, int(binary.LittleEndian.Uint16(setupHead[6:]))*4)
177	read(setup)
178	vendorLen := int(binary.LittleEndian.Uint16(setup[16:]))
179	formats := int(setup[21])
180	screenOff := 32 + (vendorLen+3)&^3 + 8*formats
181	root := binary.LittleEndian.Uint32(setup[screenOff:])
182	// Query XFIXES dynamically rather than assuming a server-specific major opcode.
183	query := []byte{98, 0, 4, 0, 6, 0, 0, 0, 'X', 'F', 'I', 'X', 'E', 'S', 0, 0}
184	write(query)
185	// Server bytes reach the client through the same tee; keep named replies as
186	// reply goldens (reply byte 0 through 32+4*length).
187	replies := map[string][]byte{}
188	keep := func(name string, p []byte) []byte {
189		t.Helper()
190		if p[0] != 1 {
191			t.Fatalf("%s: expected a reply, got %x", name, p)
192		}
193		replies[name] = p
194		return p
195	}
196	packet := func() []byte {
197		t.Helper()
198		head := make([]byte, 32)
199		read(head)
200		if head[0] == 1 {
201			tail := make([]byte, int(binary.LittleEndian.Uint32(head[4:]))*4)
202			read(tail)
203			head = append(head, tail...)
204		}
205		return head
206	}
207	ext := keep("xproto/QueryExtension", packet())
208	if ext[0] != 1 || ext[8] != 1 {
209		t.Fatalf("XFIXES unavailable: %x", ext)
210	}
211	major := ext[9]
212	version := make([]byte, 12)
213	version[0] = major
214	version[2] = 3
215	binary.LittleEndian.PutUint32(version[4:], 6)
216	write(version)
217	reply := keep("xfixes/QueryVersion", packet())
218	if reply[0] != 1 {
219		t.Fatalf("QueryVersion failed: %x", reply)
220	}
221	mapReq := make([]byte, 8)
222	mapReq[0] = 8
223	mapReq[2] = 2
224	binary.LittleEndian.PutUint32(mapReq[4:], root)
225	write(mapReq)
226	geometry := append([]byte(nil), mapReq...)
227	geometry[0] = 14
228	write(geometry)
229	geom := keep("xproto/GetGeometry", packet())
230	if geom[0] != 1 || binary.LittleEndian.Uint16(geom[2:]) != 4 {
231		t.Fatalf("MapWindow/GetGeometry rejected: %x", geom)
232	}
233
234	// InternAtom, ChangeProperty and XRes QueryClientIds exercise derived lengths,
235	// byte expressions and fixed-struct lists with independently assembled bytes.
236	intern := []byte{16, 0, 4, 0, 7, 0, 0, 0, 'X', 'T', 'U', 'R', 'B', 'O', '_', 0}
237	write(intern)
238	atomReply := keep("xproto/InternAtom", packet())
239	if atomReply[0] != 1 {
240		t.Fatalf("InternAtom: %x", atomReply)
241	}
242	atom := binary.LittleEndian.Uint32(atomReply[8:])
243	property := make([]byte, 28)
244	property[0] = 18
245	property[2] = 7
246	binary.LittleEndian.PutUint32(property[4:], root)
247	binary.LittleEndian.PutUint32(property[8:], atom)
248	binary.LittleEndian.PutUint32(property[12:], 31)
249	property[16] = 8
250	binary.LittleEndian.PutUint32(property[20:], 3)
251	copy(property[24:], "abc")
252	write(property)
253	// A successful GetProperty reply proves the preceding write was accepted.
254	get := make([]byte, 24)
255	get[0] = 20
256	get[2] = 6
257	binary.LittleEndian.PutUint32(get[4:], root)
258	binary.LittleEndian.PutUint32(get[8:], atom)
259	binary.LittleEndian.PutUint32(get[20:], 1)
260	write(get)
261	got := keep("xproto/GetProperty", packet())
262	if got[0] != 1 || string(got[32:35]) != "abc" {
263		t.Fatalf("ChangeProperty: %x", got)
264	}
265	resName := "X-Resource"
266	queryRes := make([]byte, 20)
267	queryRes[0] = 98
268	queryRes[2] = 5
269	queryRes[4] = byte(len(resName))
270	copy(queryRes[8:], resName)
271	write(queryRes)
272	resExt := packet()
273	if resExt[0] != 1 || resExt[8] != 1 {
274		t.Fatalf("XRes unavailable: %x", resExt)
275	}
276	resMajor := resExt[9]
277	resVersion := []byte{resMajor, 0, 2, 0, 1, 2, 0, 0}
278	write(resVersion)
279	if p := packet(); p[0] != 1 {
280		t.Fatalf("XRes version: %x", p)
281	}
282	ids := make([]byte, 16)
283	ids[0] = resMajor
284	ids[1] = 4
285	ids[2] = 4
286	binary.LittleEndian.PutUint32(ids[4:], 1)
287	binary.LittleEndian.PutUint32(ids[12:], 1)
288	write(ids)
289	keep("res/QueryClientIds", packet())
290	// Value-list reference packets are assembled independently, including mask-bit
291	// ordering. Two sibling windows allow all seven ConfigureWindow bits to be
292	// exercised legally. A final reply fences all void requests and exposes errors.
293	base := binary.LittleEndian.Uint32(setup[4:])
294	child, sibling, picture := base|1, base|2, base|3
295	create := func(id uint32, mask uint32, values ...uint32) {
296		p := make([]byte, 32+4*len(values))
297		p[0] = 1
298		binary.LittleEndian.PutUint16(p[2:], uint16(len(p)/4))
299		binary.LittleEndian.PutUint32(p[4:], id)
300		binary.LittleEndian.PutUint32(p[8:], root)
301		binary.LittleEndian.PutUint16(p[16:], 64)
302		binary.LittleEndian.PutUint16(p[18:], 48)
303		binary.LittleEndian.PutUint16(p[22:], 1) // InputOutput, inherited depth/visual
304		binary.LittleEndian.PutUint32(p[28:], mask)
305		for i, v := range values {
306			binary.LittleEndian.PutUint32(p[32+4*i:], v)
307		}
308		write(p)
309	}
310	create(sibling, 0)
311	create(child, 2|512|1024, 0x123456, 1, 0)
312	change := make([]byte, 20)
313	change[0], change[2] = 2, 5
314	binary.LittleEndian.PutUint32(change[4:], child)
315	binary.LittleEndian.PutUint32(change[8:], 512|1024)
316	binary.LittleEndian.PutUint32(change[16:], 1) // false, true BOOL32 words
317	write(change)
318	configure := func(mask uint16, values ...uint32) {
319		p := make([]byte, 12+4*len(values))
320		p[0] = 12
321		binary.LittleEndian.PutUint16(p[2:], uint16(len(p)/4))
322		binary.LittleEndian.PutUint32(p[4:], child)
323		binary.LittleEndian.PutUint16(p[8:], mask)
324		for i, v := range values {
325			binary.LittleEndian.PutUint32(p[12+4*i:], v)
326		}
327		write(p)
328	}
329	configure(0)
330	configure(1|4|64, 0xfffffff9, 80, 0)
331	configure(127, 0xfffffffd, 9, 96, 72, 2, sibling, 0)
332	queryRender := []byte{98, 0, 4, 0, 6, 0, 0, 0, 'R', 'E', 'N', 'D', 'E', 'R', 0, 0}
333	write(queryRender)
334	renderExt := packet()
335	if renderExt[0] != 1 || renderExt[8] != 1 {
336		t.Fatalf("value requests/RENDER query: %x", renderExt)
337	}
338	renderMajor := renderExt[9]
339	write([]byte{renderMajor, 1, 1, 0}) // QueryPictFormats
340	formatsReply := keep("render/QueryPictFormats", packet())
341	if formatsReply[0] != 1 {
342		t.Fatalf("QueryPictFormats: %x", formatsReply)
343	}
344	// Match the root visual rather than guessing a server-assigned format ID.
345	rootVisual := binary.LittleEndian.Uint32(setup[screenOff+32:])
346	off := 32 + 28*int(binary.LittleEndian.Uint32(formatsReply[8:]))
347	var format uint32
348	for range binary.LittleEndian.Uint32(formatsReply[12:]) {
349		depths := binary.LittleEndian.Uint32(formatsReply[off:])
350		off += 8
351		for range depths {
352			visuals := binary.LittleEndian.Uint16(formatsReply[off+2:])
353			off += 8
354			for range visuals {
355				if binary.LittleEndian.Uint32(formatsReply[off:]) == rootVisual {
356					format = binary.LittleEndian.Uint32(formatsReply[off+4:])
357				}
358				off += 8
359			}
360		}
361	}
362	if format == 0 {
363		t.Fatal("root visual has no Render format")
364	}
365	pic := make([]byte, 32)
366	pic[0], pic[1], pic[2] = renderMajor, 4, 8
367	binary.LittleEndian.PutUint32(pic[4:], picture)
368	binary.LittleEndian.PutUint32(pic[8:], child)
369	binary.LittleEndian.PutUint32(pic[12:], format)
370	binary.LittleEndian.PutUint32(pic[16:], 1|16|32)
371	binary.LittleEndian.PutUint32(pic[20:], 1) // RepeatNormal
372	binary.LittleEndian.PutUint32(pic[24:], 0xfffffffb)
373	binary.LittleEndian.PutUint32(pic[28:], 7)
374	write(pic)
375	write([]byte{43, 0, 1, 0}) // GetInputFocus fences CreatePicture
376	if p := packet(); p[0] != 1 {
377		t.Fatalf("CreatePicture rejected: %x", p)
378	}
379	// Reply-only captures. Opcodes 3, 15, 20 and 99 are not request goldens, so
380	// the request-stream classifier below is unaffected.
381	attrs := make([]byte, 8)
382	attrs[0], attrs[2] = 3, 2
383	binary.LittleEndian.PutUint32(attrs[4:], child)
384	write(attrs)
385	keep("xproto/GetWindowAttributes", packet())
386	tree := make([]byte, 8)
387	tree[0], tree[2] = 15, 2
388	binary.LittleEndian.PutUint32(tree[4:], root)
389	write(tree)
390	keep("xproto/QueryTree", packet())
391	// WM_NAME is unset on a fresh Xvfb root: a zero-length GetProperty reply.
392	empty := make([]byte, 24)
393	empty[0], empty[2] = 20, 6
394	binary.LittleEndian.PutUint32(empty[4:], root)
395	binary.LittleEndian.PutUint32(empty[8:], 39)
396	binary.LittleEndian.PutUint32(empty[20:], 1)
397	write(empty)
398	keep("xproto/GetPropertyEmpty", packet())
399	write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator
400	keep("xproto/ListExtensions", packet())
401	// Event and error captures. The tee client acts as a window manager on the
402	// root; a second raw client (direct socket, not teed) triggers the events.
403	// Requests sent here on the tee use window/minor values the request-golden
404	// classifier below ignores.
405	xfixesEvent, xfixesError := ext[10], ext[11]
406	events := map[string][]byte{}
407	xerrors := map[string][]byte{}
408	// collect reads events and errors until the reply fencing them.
409	collect := func(names map[byte]string) {
410		t.Helper()
411		write([]byte{43, 0, 1, 0}) // GetInputFocus
412		for {
413			p := packet()
414			switch {
415			case p[0] == 1:
416				return
417			case p[0] == 0:
418				name := names[p[1]]
419				if name == "" {
420					t.Fatalf("unexpected error %x", p)
421				}
422				xerrors[name] = p
423			default:
424				name := names[p[0]]
425				if name == "" {
426					t.Fatalf("unexpected event %x", p)
427				}
428				events[name] = p
429			}
430		}
431	}
432	const (
433		maskSubstructureNotify                 = 1 << 19
434		maskSubstructureRedirect               = 1 << 20
435		maskPropertyChange                     = 1 << 22
436		atomPrimary, atomSecondary, atomString = 1, 2, 31
437	)
438	selectRoot := make([]byte, 16)
439	selectRoot[0], selectRoot[2] = 2, 4
440	binary.LittleEndian.PutUint32(selectRoot[4:], root)
441	binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask
442	binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange)
443	write(selectRoot)
444	selectSel := make([]byte, 16)
445	selectSel[0], selectSel[1], selectSel[2] = major, 2, 4
446	binary.LittleEndian.PutUint32(selectSel[4:], root)
447	binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary)
448	binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner
449	write(selectSel)
450	collect(nil)
451
452	other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second)
453	if err != nil {
454		t.Fatal(err)
455	}
456	defer other.Close()
457	_ = other.SetDeadline(time.Now().Add(5 * time.Second))
458	send := func(p []byte) {
459		t.Helper()
460		if _, err := other.Write(p); err != nil {
461			t.Fatal(err)
462		}
463	}
464	recv := func(p []byte) {
465		t.Helper()
466		if _, err := io.ReadFull(other, p); err != nil {
467			t.Fatal(err)
468		}
469	}
470	// sync round-trips the second client; any error fails the capture.
471	sync := func() {
472		t.Helper()
473		send([]byte{43, 0, 1, 0})
474		var p [32]byte
475		recv(p[:])
476		if p[0] != 1 {
477			t.Fatalf("second client: %x", p)
478		}
479	}
480	send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0})
481	var otherHead [8]byte
482	recv(otherHead[:])
483	if otherHead[0] != 1 {
484		t.Fatalf("second setup failed: %x", otherHead)
485	}
486	otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4)
487	recv(otherSetup)
488	client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1
489	win := make([]byte, 32)
490	win[0], win[2] = 1, 8
491	binary.LittleEndian.PutUint32(win[4:], client2)
492	binary.LittleEndian.PutUint32(win[8:], root)
493	binary.LittleEndian.PutUint16(win[12:], 10)
494	binary.LittleEndian.PutUint16(win[14:], 20)
495	binary.LittleEndian.PutUint16(win[16:], 300)
496	binary.LittleEndian.PutUint16(win[18:], 200)
497	binary.LittleEndian.PutUint16(win[22:], 1)
498	send(win) // CreateNotify
499	mapOther := make([]byte, 8)
500	mapOther[0], mapOther[2] = 8, 2
501	binary.LittleEndian.PutUint32(mapOther[4:], client2)
502	send(mapOther) // MapRequest (redirected)
503	cfg := make([]byte, 24)
504	cfg[0], cfg[2] = 12, 6
505	binary.LittleEndian.PutUint32(cfg[4:], client2)
506	binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width
507	binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb)
508	binary.LittleEndian.PutUint32(cfg[16:], 40)
509	binary.LittleEndian.PutUint32(cfg[20:], 640)
510	send(cfg) // ConfigureRequest (redirected)
511	prop := make([]byte, 28)
512	prop[0], prop[2] = 18, 7
513	binary.LittleEndian.PutUint32(prop[4:], root)
514	binary.LittleEndian.PutUint32(prop[8:], atom)
515	binary.LittleEndian.PutUint32(prop[12:], atomString)
516	prop[16] = 8
517	binary.LittleEndian.PutUint32(prop[20:], 2)
518	copy(prop[24:], "hi")
519	send(prop) // PropertyNotify (NewValue)
520	msg := make([]byte, 44)
521	msg[0], msg[2] = 25, 11
522	binary.LittleEndian.PutUint32(msg[4:], root)
523	binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect)
524	msg[12], msg[13] = 33, 32 // ClientMessage, format 32
525	binary.LittleEndian.PutUint32(msg[16:], client2)
526	binary.LittleEndian.PutUint32(msg[20:], atom)
527	for i := range 5 {
528		binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1)))
529	}
530	send(msg) // ClientMessage (SendEvent bit set)
531	owner := make([]byte, 16)
532	owner[0], owner[2] = 22, 4
533	binary.LittleEndian.PutUint32(owner[4:], client2)
534	binary.LittleEndian.PutUint32(owner[8:], atomPrimary)
535	send(owner) // xfixes SelectionNotify (SetSelectionOwner)
536	sync()
537	collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest",
538		28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"})
539	// The window manager maps the window itself (MapNotify); the second client
540	// then unmaps and destroys it.
541	mapByWM := make([]byte, 8)
542	mapByWM[0], mapByWM[2] = 8, 2
543	binary.LittleEndian.PutUint32(mapByWM[4:], client2)
544	write(mapByWM)
545	collect(map[byte]string{19: "xproto/MapNotify"})
546	unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0}
547	binary.LittleEndian.PutUint32(unmap[4:], client2)
548	send(unmap)
549	destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0}
550	binary.LittleEndian.PutUint32(destroy[4:], client2)
551	send(destroy)
552	sync()
553	collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"})
554	// Core SelectionNotify: SECONDARY has no owner, so the server replies itself.
555	convert := make([]byte, 24)
556	convert[0], convert[2] = 24, 6
557	binary.LittleEndian.PutUint32(convert[4:], child)
558	binary.LittleEndian.PutUint32(convert[8:], atomSecondary)
559	binary.LittleEndian.PutUint32(convert[12:], atomString)
560	binary.LittleEndian.PutUint32(convert[16:], atom)
561	write(convert)
562	collect(map[byte]string{31: "xproto/SelectionNotify"})
563	// Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion).
564	badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d}
565	write(badAtom)
566	badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0}
567	binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777)
568	write(badWindow)
569	badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0}
570	binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888)
571	write(badRegion)
572	collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"})
573	if len(events) != 10 || len(xerrors) != 3 {
574		t.Fatalf("captured %d events, %d errors", len(events), len(xerrors))
575	}
576	// Closing the client drains the request direction; wait before inspecting tee.
577	client.Close()
578	select {
579	case err := <-copied:
580		if err != nil {
581			t.Fatal(err)
582		}
583	case <-time.After(5 * time.Second):
584		t.Fatal("tee did not drain")
585	}
586	stream := recorded.Bytes()
587	requests := stream[12:]
588	captured := map[string][]byte{}
589	configIndex := 0
590	for len(requests) > 0 {
591		size := int(binary.LittleEndian.Uint16(requests[2:])) * 4
592		if size < 4 || size > len(requests) {
593			t.Fatal("malformed capture")
594		}
595		p := requests[:size]
596		switch p[0] {
597		case 1:
598			if binary.LittleEndian.Uint32(p[4:]) == child {
599				captured["xproto/CreateWindow"] = p
600			}
601		case 2:
602			if binary.LittleEndian.Uint32(p[4:]) == child {
603				captured["xproto/ChangeWindowAttributes"] = p
604			}
605		case 12:
606			names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"}
607			captured["xproto/"+names[configIndex]] = p
608			configIndex++
609		case 8:
610			if binary.LittleEndian.Uint32(p[4:]) == root {
611				captured["xproto/MapWindow"] = p
612			}
613		case 14:
614			captured["xproto/GetGeometry"] = p
615		case 16:
616			captured["xproto/InternAtom"] = p
617		case 18:
618			captured["xproto/ChangeProperty"] = p
619		default:
620			if p[0] == renderMajor && p[1] == 4 {
621				captured["render/CreatePicture"] = p
622			}
623			if p[0] == resMajor && p[1] == 4 {
624				captured["res/QueryClientIds"] = p
625			}
626			if p[0] == major && p[1] == 0 {
627				captured["xfixes/QueryVersion"] = p
628			}
629		}
630		requests = requests[size:]
631	}
632	if len(captured) != 12 || len(replies) != 11 {
633		t.Fatal("missing captured requests or replies")
634	}
635	out := map[string][]byte{}
636	for name, p := range replies {
637		out[name+".reply"] = p
638	}
639	for name, p := range captured {
640		out[name+".req"] = p
641	}
642	for name, p := range events {
643		out[name+".event"] = p
644	}
645	for name, p := range xerrors {
646		out[name+".error"] = p
647	}
648	// Server-chosen values, keyed by the roles used in serverFields.
649	roles := map[string]uint32{
650		"root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture,
651		"client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888,
652		"rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format,
653		"xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError),
654		"resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor),
655	}
656	if *update {
657		for name, p := range out {
658			writeGolden(t, name, p)
659		}
660		rolesJSON, err := json.MarshalIndent(roles, "", "  ")
661		if err != nil {
662			t.Fatal(err)
663		}
664		if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil {
665			t.Fatal(err)
666		}
667		return
668	}
669	rolesJSON, err := os.ReadFile(rolesPath)
670	if err != nil {
671		t.Fatal(err)
672	}
673	var capturedRoles map[string]uint32
674	if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil {
675		t.Fatal(err)
676	}
677	for _, role := range slices.Sorted(maps.Keys(roles)) {
678		if roles[role] != capturedRoles[role] {
679			t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role])
680		}
681	}
682	for name, p := range out {
683		compareGolden(t, name, p, roles, capturedRoles)
684	}
685}
686
687var sources = map[string]string{
688	"req":   "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n",
689	"reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n",
690	"event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n",
691	"error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n",
692}
693
694func writeGolden(t *testing.T, name string, p []byte) {
695	t.Helper()
696	path := filepath.Join("../../testdata/golden", name+".hex")
697	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
698		t.Fatal(err)
699	}
700	source := sources[name[strings.LastIndexByte(name, '.')+1:]]
701	text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n"
702	if err := os.WriteFile(path, []byte(text), 0644); err != nil {
703		t.Fatal(err)
704	}
705}
706
707// compareGolden checks a live packet against the committed golden without
708// depending on the server build: server-chosen fields must hold each side's
709// own value (live roles, recorded roles) and are masked before an exact
710// comparison; structural replies are checked for invariants only.
711func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) {
712	t.Helper()
713	want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex"))
714	if err != nil {
715		t.Errorf("%s: %v (run with -update to create it)", name, err)
716		return
717	}
718	if check := structural[name]; check != nil {
719		if err := check(live, roles); err != nil {
720			t.Errorf("%s (live): %v", name, err)
721		}
722		if err := check(want, recorded); err != nil {
723			t.Errorf("%s (golden): %v", name, err)
724		}
725		return
726	}
727	got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil)
728	for _, e := range []struct {
729		side string
730		err  error
731	}{{"live", got.err}, {"golden", exp.err}} {
732		if e.err != nil {
733			t.Errorf("%s (%s): %v", name, e.side, e.err)
734			err = e.err
735		}
736	}
737	if err == nil && !bytes.Equal(got.b, exp.b) {
738		t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want)
739	}
740}
741
742type normalized struct {
743	b   []byte
744	err error
745}
746
747// normalize verifies each server-chosen field of p holds roles[Role] and
748// replaces it with zero bytes.
749func normalize(name string, p []byte, roles map[string]uint32) normalized {
750	b := bytes.Clone(p)
751	for _, f := range serverFields[name] {
752		if f.Off+f.Size > len(b) {
753			return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))}
754		}
755		if f.Role != "time" {
756			v, ok := roles[f.Role]
757			if !ok {
758				return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)}
759			}
760			var got uint32
761			if f.Size == 1 {
762				got = uint32(b[f.Off])
763			} else {
764				got = binary.LittleEndian.Uint32(b[f.Off:])
765			}
766			if got != v {
767				return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)}
768			}
769		}
770		clear(b[f.Off : f.Off+f.Size])
771	}
772	return normalized{b: b}
773}
774
775// checkListExtensions: a framed reply whose STR list holds exactly names_len
776// entries filling the reply, including the extensions xturbo binds.
777func checkListExtensions(p []byte, _ map[string]uint32) error {
778	if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) {
779		return fmt.Errorf("bad framing")
780	}
781	names := map[string]bool{}
782	off := 32
783	for range int(p[1]) {
784		if off >= len(p) || off+1+int(p[off]) > len(p) {
785			return fmt.Errorf("STR list overruns the reply")
786		}
787		names[string(p[off+1:off+1+int(p[off])])] = true
788		off += 1 + int(p[off])
789	}
790	if (off+3)&^3 != len(p) {
791		return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p))
792	}
793	for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} {
794		if !names[want] {
795			return fmt.Errorf("missing extension %q", want)
796		}
797	}
798	return nil
799}
800
801// checkQueryPictFormats: declared counts match the list sizes exactly and the
802// root visual maps to the picture format CreatePicture used.
803func checkQueryPictFormats(p []byte, roles map[string]uint32) error {
804	u32 := binary.LittleEndian.Uint32
805	if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) {
806		return fmt.Errorf("bad framing")
807	}
808	formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:]))
809	off := 32 + 28*formats
810	found := false
811	for range screens {
812		if off+8 > len(p) {
813			return fmt.Errorf("screens overrun")
814		}
815		depths := int(u32(p[off:]))
816		off += 8
817		for range depths {
818			if off+8 > len(p) {
819				return fmt.Errorf("depths overrun")
820			}
821			visuals := int(binary.LittleEndian.Uint16(p[off+2:]))
822			off += 8
823			for range visuals {
824				if off+8 > len(p) {
825					return fmt.Errorf("visuals overrun")
826				}
827				if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] {
828					found = true
829				}
830				off += 8
831			}
832		}
833	}
834	off += 4 * subpixels
835	if off != len(p) {
836		return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p))
837	}
838	if !found {
839		return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"])
840	}
841	return nil
842}

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 1

↑↓ moveenter openesc close