detached 602baa7

bootstrap_xserver_test.go RawHistoryFolder

1//go:build xserver
2
3package xturbo_test
4
5import (
6	"crypto/rand"
7	"errors"
8	"path/filepath"
9	"testing"
10
11	"bnema.dev/xturbo"
12	"bnema.dev/xturbo/xproto"
13)
14
15func dialXvfb(t *testing.T) *xturbo.Conn {
16	t.Helper()
17	dir := t.TempDir()
18	auth := filepath.Join(dir, "Xauthority")
19	cookie := make([]byte, 16)
20	_, _ = rand.Read(cookie)
21	writeXauthority(t, auth, cookie)
22	display := startXvfb(t, auth)
23	t.Setenv("XAUTHORITY", auth)
24	c, err := xturbo.Dial(display)
25	if err != nil {
26		t.Fatal(err)
27	}
28	t.Cleanup(func() { _ = c.Close() })
29	return c
30}
31
32// The ExtTable filled at connect matches what independent QueryExtension
33// round trips through the generated encoder report.
34func TestXserverExtTable(t *testing.T) {
35	c := dialXvfb(t)
36	const kindQuery = 16
37	type got struct {
38		present                  bool
39		major, firstEv, firstErr uint8
40	}
41	answers := map[uint32]got{}
42	h := xturbo.Handler{OnReply: func(r *xturbo.Reply) {
43		q, ok := xproto.ParseQueryExtensionReply(r.Raw)
44		if !ok {
45			t.Error("short QueryExtension reply")
46			return
47		}
48		answers[r.Args[0]] = got{q.Present(), q.MajorOpcode(), q.FirstEvent(), q.FirstError()}
49	}}
50	for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ {
51		seq := xproto.QueryExtension(c.Buf(), xturbo.ExtXName[id])
52		if err := c.Expect(seq, kindQuery, uint32(id), 0, 0, 0); err != nil {
53			t.Fatal(err)
54		}
55	}
56	if err := c.Sync(&h); err != nil {
57		t.Fatal(err)
58	}
59	for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ {
60		a, ok := answers[uint32(id)]
61		info := c.Ext().Info(id)
62		if !ok {
63			t.Fatalf("%s: no reply", xturbo.ExtXName[id])
64		}
65		want := xturbo.ExtInfo{}
66		if a.present {
67			want = xturbo.ExtInfo{Present: true, Major: a.major, FirstEvent: a.firstEv, FirstError: a.firstErr}
68		}
69		if info != want {
70			t.Errorf("%s: table %+v, server %+v", xturbo.ExtXName[id], info, a)
71		}
72	}
73	// Xvfb builds XFIXES, Composite, RENDER, SHAPE, X-Resource, XC-MISC and
74	// BIG-REQUESTS in; the XWM depends on them.
75	for _, id := range []xturbo.ExtID{xturbo.ExtXFixes, xturbo.ExtComposite, xturbo.ExtRender, xturbo.ExtShape, xturbo.ExtRes, xturbo.ExtXCMisc, xturbo.ExtBigRequests} {
76		if !c.Ext().Info(id).Present {
77			t.Errorf("%s absent", xturbo.ExtXName[id])
78		}
79	}
80	if st := c.Stats(); st.DroppedReplies != 0 {
81		t.Fatal(st)
82	}
83}
84
85// InternAtoms agrees with independent InternAtom round trips and with the
86// predefined atoms; errors come back as *XError.
87func TestXserverInternAtoms(t *testing.T) {
88	c := dialXvfb(t)
89	names := []string{"WM_NAME", "STRING", "WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING", "_NET_SUPPORTED", "WM_STATE", "XTURBO_TEST_ATOM"}
90	dst := make([]uint32, len(names))
91	if err := c.InternAtoms(names, false, dst); err != nil {
92		t.Fatal(err)
93	}
94	if dst[0] != uint32(xproto.AtomWmName) || dst[1] != uint32(xproto.AtomString) {
95		t.Fatalf("predefined atoms %v", dst[:2])
96	}
97	const kindAtom = 16
98	again := make([]uint32, len(names))
99	h := xturbo.Handler{OnReply: func(r *xturbo.Reply) {
100		a, ok := xproto.ParseInternAtomReply(r.Raw)
101		if ok {
102			again[r.Args[0]] = uint32(a.Atom())
103		}
104	}}
105	for i, name := range names {
106		seq := xproto.InternAtom(c.Buf(), true, name)
107		if err := c.Expect(seq, kindAtom, uint32(i), 0, 0, 0); err != nil {
108			t.Fatal(err)
109		}
110	}
111	if err := c.Sync(&h); err != nil {
112		t.Fatal(err)
113	}
114	seen := map[uint32]bool{}
115	for i := range names {
116		if dst[i] == 0 || dst[i] != again[i] || seen[dst[i]] {
117			t.Fatalf("%s: %d vs %d", names[i], dst[i], again[i])
118		}
119		seen[dst[i]] = true
120	}
121	// onlyIfExists on an unknown name gives 0, no error.
122	none := make([]uint32, 1)
123	if err := c.InternAtoms([]string{"XTURBO_NEVER_INTERNED_ATOM"}, true, none); err != nil || none[0] != 0 {
124		t.Fatal(err, none)
125	}
126}
127
128// NewXID hands out many distinct ids in the client's range; windows created
129// with them exist and can be destroyed and the ids reused.
130func TestXserverNewXIDWindows(t *testing.T) {
131	c := dialXvfb(t)
132	const n = 2000
133	ids := make([]uint32, 0, n)
134	seen := map[uint32]bool{}
135	for range n {
136		id, err := c.NewXID()
137		if err != nil {
138			t.Fatal(err)
139		}
140		if id == 0 || seen[id] {
141			t.Fatalf("id %#x", id)
142		}
143		seen[id] = true
144		ids = append(ids, id)
145	}
146	b := c.Buf()
147	var v xproto.CreateWindowValues
148	for _, id := range ids[:64] {
149		xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 1, 2, 30, 40, 0, xproto.WindowClassInputOutput, 0, &v)
150	}
151	var xerrs []xturbo.XError
152	geoms := 0
153	const kindGeom = 16
154	h := xturbo.Handler{
155		OnReply: func(r *xturbo.Reply) {
156			if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok && g.Width() == 30 && g.Height() == 40 {
157				geoms++
158			}
159		},
160		OnError: func(e *xturbo.XError) { xerrs = append(xerrs, *e) },
161	}
162	for _, id := range ids[:64] {
163		seq := xproto.GetGeometry(b, xproto.Drawable(id))
164		if err := c.Expect(seq, kindGeom, id, 0, 0, 0); err != nil {
165			t.Fatal(err)
166		}
167	}
168	if err := c.Sync(&h); err != nil {
169		t.Fatal(err)
170	}
171	if geoms != 64 || len(xerrs) != 0 {
172		t.Fatalf("geometries %d errors %v", geoms, xerrs)
173	}
174	for _, id := range ids[:64] {
175		xproto.DestroyWindow(b, xproto.Window(id))
176		c.FreeXID(id)
177	}
178	if err := c.Sync(&h); err != nil || len(xerrs) != 0 {
179		t.Fatal(err, xerrs)
180	}
181	// A reused id can be created again: the destroy completed.
182	id, err := c.NewXID()
183	if err != nil || !seen[id] {
184		t.Fatalf("reuse %#x %v", id, err)
185	}
186	xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v)
187	xproto.DestroyWindow(b, xproto.Window(id))
188	if err := c.Sync(&h); err != nil || len(xerrs) != 0 {
189		t.Fatal(err, xerrs)
190	}
191}
192
193// Exhausting the setup range triggers an XC-MISC GetXIDRange refill; the
194// refilled ids are usable.
195func TestXserverXIDRefill(t *testing.T) {
196	c := dialXvfb(t)
197	xturbo.ExhaustXIDs(c)
198	id, err := c.NewXID()
199	if err != nil {
200		t.Fatal(err)
201	}
202	var v xproto.CreateWindowValues
203	xproto.CreateWindow(c.Buf(), 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v)
204	xproto.DestroyWindow(c.Buf(), xproto.Window(id))
205	var xerr *xturbo.XError
206	h := xturbo.Handler{OnError: func(e *xturbo.XError) { cp := *e; xerr = &cp }}
207	if err := c.Sync(&h); err != nil || xerr != nil {
208		t.Fatal(err, xerr)
209	}
210	// Non-blocking prefetch: queued from the loop, used after the next
211	// exhaustion without a blocking round trip.
212	xturbo.ExhaustXIDs(c)
213	if !c.XIDsLow() {
214		t.Fatal("not low after exhaustion")
215	}
216	if err := c.PrefetchXIDs(); err != nil {
217		t.Fatal(err)
218	}
219	if err := c.Sync(&h); err != nil || c.XIDsLow() {
220		t.Fatal(err, c.XIDsLow())
221	}
222	// The server hands out its lowest free range, which may hold the id just
223	// destroyed: check the id is usable, not that it is new.
224	id2, err := c.NewXID()
225	if err != nil {
226		t.Fatal(err)
227	}
228	xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v)
229	xproto.DestroyWindow(c.Buf(), xproto.Window(id2))
230	if err := c.Sync(&h); err != nil || xerr != nil {
231		t.Fatal(err, xerr)
232	}
233}
234
235// A long silent stream gets fences; the connection keeps working.
236func TestXserverAutoResync(t *testing.T) {
237	c := dialXvfb(t)
238	b := c.Buf()
239	for range 3 * 0x8000 {
240		xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event
241		if b.Len() > 32<<10 {
242			if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) {
243				t.Fatal(err)
244			}
245			if err := c.Dispatch(nil); err != nil {
246				t.Fatal(err)
247			}
248		}
249	}
250	if b.Err() != nil {
251		t.Fatal(b.Err())
252	}
253	if err := c.Sync(nil); err != nil {
254		t.Fatal(err)
255	}
256	if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 {
257		t.Fatalf("%+v", st)
258	}
259}

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