detached 602baa7

bootstrap_test.go RawHistoryFolder

1package xturbo
2
3import (
4	"encoding/binary"
5	"errors"
6	"testing"
7	"time"
8
9	"golang.org/x/sys/unix"
10)
11
12// waitRequests waits until the scripted server has read n requests.
13func waitRequests(t *testing.T, srv *scriptConn, n int) {
14	t.Helper()
15	deadline := time.Now().Add(5 * time.Second)
16	for len(srv.requests()) < n {
17		if time.Now().After(deadline) {
18			t.Fatalf("server saw %d requests, want %d", len(srv.requests()), n)
19		}
20		time.Sleep(time.Millisecond)
21	}
22}
23
24func TestBootstrapAllExtensions(t *testing.T) {
25	exts := fullExts()
26	c, srv := dialScript(t, &script{exts: exts, bigreqMax: 0x3fffff})
27	for id := ExtCore + 1; id < extCount; id++ {
28		e := exts[ExtXName[id]]
29		want := ExtInfo{true, e.major, e.firstEvent, e.firstError}
30		if got := c.Ext().Info(id); got != want {
31			t.Errorf("%s: %+v, want %+v", ExtXName[id], got, want)
32		}
33	}
34	if id, rel, ok := c.Ext().ByEvent(88); !ok || id != ExtXFixes || rel != 1 {
35		t.Fatal("reverse event map", id, rel, ok)
36	}
37	if c.buf.maxReq != 0x3fffff*4 {
38		t.Fatalf("maxReq %d", c.buf.maxReq)
39	}
40	// One QueryExtension per extension, then Enable (queued once
41	// BIG-REQUESTS answered), fences; nothing left registered.
42	reqs := srv.requests()
43	for i := range int(extCount) - 1 {
44		if reqs[i].major != opQueryExtension {
45			t.Fatalf("request %d major %d", i, reqs[i].major)
46		}
47	}
48	var enables int
49	for _, r := range reqs {
50		if r.major == 133 {
51			enables++
52		}
53	}
54	if enables != 1 || c.live != 0 || c.Stats().DroppedReplies != 0 {
55		t.Fatalf("enables %d live %d %+v", enables, c.live, c.Stats())
56	}
57	// A big request now encodes with the 8-byte header.
58	if p, _ := c.Buf().Request(18, 0, coreMaxReq); p == nil {
59		t.Fatal(c.Buf().Err())
60	}
61}
62
63func TestBootstrapAbsentExtensions(t *testing.T) {
64	exts := fullExts()
65	delete(exts, "BIG-REQUESTS")
66	delete(exts, "Composite")
67	c, srv := dialScript(t, &script{exts: exts})
68	if c.Ext().Info(ExtComposite).Present || c.Ext().Major(ExtBigRequests) != 0 {
69		t.Fatal("absent extension marked present")
70	}
71	if !c.Ext().Info(ExtXFixes).Present {
72		t.Fatal("XFIXES missing")
73	}
74	if c.buf.maxReq != coreMaxReq {
75		t.Fatalf("maxReq %d without BIG-REQUESTS", c.buf.maxReq)
76	}
77	for _, r := range srv.requests() {
78		if r.major >= 128 {
79			t.Fatalf("extension request %d sent", r.major)
80		}
81	}
82	if p, _ := c.Buf().Request(18, 0, coreMaxReq); p != nil || !errors.Is(c.Buf().Err(), ErrRequestTooLarge) {
83		t.Fatal("big request without BIG-REQUESTS")
84	}
85}
86
87func TestBootstrapErrorFailsConnect(t *testing.T) {
88	fd, _ := startScript(t, &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
89		if major == opQueryExtension {
90			s.xerror(11, major, 0)
91			return true
92		}
93		return false
94	}})
95	c, err := NewConn(fd)
96	var xe *XError
97	if c != nil || !errors.As(err, &xe) || xe.Kind != kindQueryExtension || xe.Code != 11 {
98		t.Fatalf("conn %v err %v", c != nil, err)
99	}
100}
101
102func TestInternAtoms(t *testing.T) {
103	sc := &script{atoms: map[string]uint32{"WM_STATE": 300, "_NET_WM_NAME": 301}}
104	c, srv := dialScript(t, sc)
105	before := len(srv.requests())
106	names := []string{"_NET_WM_NAME", "NEW_ONE", "WM_STATE", "NEW_TWO"}
107	dst := make([]uint32, len(names))
108	if err := c.InternAtoms(names, false, dst); err != nil {
109		t.Fatal(err)
110	}
111	if dst[0] != 301 || dst[2] != 300 || dst[1] == 0 || dst[3] == 0 || dst[1] == dst[3] {
112		t.Fatal(dst)
113	}
114	if dst[1] != srv.atom("NEW_ONE") || dst[3] != srv.atom("NEW_TWO") {
115		t.Fatal("atoms out of order", dst)
116	}
117	// Pipelined in name order, then one fence.
118	reqs := srv.requests()[before:]
119	if len(reqs) != len(names)+1 || reqs[len(names)].major != opGetInputFocus {
120		t.Fatalf("%d requests", len(reqs))
121	}
122	for i, r := range reqs[:len(names)] {
123		if r.major != opInternAtom || string(r.raw[8:8+len(names[i])]) != names[i] {
124			t.Fatalf("request %d: %x", i, r.raw)
125		}
126	}
127	// onlyIfExists: unknown names give 0.
128	dst2 := make([]uint32, 2)
129	if err := c.InternAtoms([]string{"WM_STATE", "NOPE"}, true, dst2); err != nil || dst2[0] != 300 || dst2[1] != 0 {
130		t.Fatal(err, dst2)
131	}
132	if err := c.InternAtoms(names, false, dst[:1]); !errors.Is(err, ErrBadLength) {
133		t.Fatal(err)
134	}
135	if err := c.InternAtoms(nil, false, nil); err != nil {
136		t.Fatal(err)
137	}
138}
139
140func TestInternAtomsError(t *testing.T) {
141	sc := &script{badAtoms: map[string]bool{"BAD": true}}
142	c, _ := dialScript(t, sc)
143	var userErrs int
144	h := Handler{OnError: func(*XError) { userErrs++ }}
145	if err := c.Dispatch(&h); err != nil {
146		t.Fatal(err)
147	}
148	dst := make([]uint32, 3)
149	err := c.InternAtoms([]string{"A", "BAD", "C"}, false, dst)
150	var xe *XError
151	if !errors.As(err, &xe) || xe.Code != 11 || xe.Args[0] != 1 || xe.Kind != kindInternAtom || xe.Major != opInternAtom {
152		t.Fatalf("%v", err)
153	}
154	if dst[0] == 0 || dst[1] != 0 || dst[2] == 0 || userErrs != 0 {
155		t.Fatal(dst, userErrs)
156	}
157	// The error does not stick to the next call.
158	if err := c.InternAtoms([]string{"D"}, false, dst[:1]); err != nil {
159		t.Fatal(err)
160	}
161}
162
163func TestInternAtomsManyWrapsRing(t *testing.T) {
164	c, _ := dialScript(t, &script{})
165	names := make([]string, 3*ringSize+7)
166	for i := range names {
167		names[i] = "ATOM_" + string(rune('A'+i%26)) + string(rune('a'+i/26%26)) + string(rune('0'+i/676))
168	}
169	dst := make([]uint32, len(names))
170	if err := c.InternAtoms(names, false, dst); err != nil {
171		t.Fatal(err)
172	}
173	seen := map[uint32]bool{}
174	for i, a := range dst {
175		if a == 0 || seen[a] {
176			t.Fatalf("atom %d = %d", i, a)
177		}
178		seen[a] = true
179	}
180}
181
182// Sync delivers user packets that arrive meanwhile to h, waits for user
183// registrations and keeps errors on reserved requests away from OnError.
184func TestSyncInterleavedEvents(t *testing.T) {
185	const kindProbe = kindUser + 3
186	sc := &script{}
187	sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
188		switch major {
189		case 8: // MapWindow: emit a MapNotify and a MapRequest
190			s.event(19, 1)
191			s.event(20, 2)
192			return true
193		case 14: // GetGeometry: answer late, after an event
194			s.event(28, 3)
195			s.reply(24, make([]byte, 16))
196			return true
197		case 10: // UnmapWindow: user error
198			s.xerror(3, major, 0x42)
199			return true
200		}
201		return false
202	}
203	c, _ := dialScript(t, sc)
204	var events []EventKind
205	var replies []uint16
206	var errs []uint8
207	h := Handler{
208		OnEvent: func(ev *Event) { events = append(events, ev.Kind) },
209		OnReply: func(r *Reply) { replies = append(replies, r.Kind) },
210		OnError: func(e *XError) { errs = append(errs, e.Code) },
211	}
212	b := c.Buf()
213	p, _ := b.Request(8, 0, 4)
214	binary.LittleEndian.PutUint32(p, 7)
215	_, geom := b.Request(14, 0, 4)
216	if err := c.Expect(geom, kindProbe, 0, 0, 0, 0); err != nil {
217		t.Fatal(err)
218	}
219	b.Request(10, 0, 4)
220	if err := c.Sync(&h); err != nil {
221		t.Fatal(err)
222	}
223	if len(events) != 3 || events[0] != 19 || events[1] != 20 || events[2] != 28 {
224		t.Fatal(events)
225	}
226	if len(replies) != 1 || replies[0] != kindProbe || len(errs) != 1 || errs[0] != 3 || c.live != 0 {
227		t.Fatal(replies, errs, c.live)
228	}
229}
230
231// A callback registering more requests during Sync gets them waited for too.
232func TestSyncFollowsCallbackRequests(t *testing.T) {
233	const kindChain = kindUser + 4
234	sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
235		if major == 14 {
236			s.reply(0, nil)
237			return true
238		}
239		return false
240	}}
241	c, _ := dialScript(t, sc)
242	chain := 0
243	var h Handler
244	h.OnReply = func(r *Reply) {
245		chain++
246		if chain < 3 {
247			_, seq := c.Buf().Request(14, 0, 4)
248			if err := c.Expect(seq, kindChain, 0, 0, 0, 0); err != nil {
249				t.Error(err)
250			}
251		}
252	}
253	_, seq := c.Buf().Request(14, 0, 4)
254	_ = c.Expect(seq, kindChain, 0, 0, 0, 0)
255	if err := c.Sync(&h); err != nil {
256		t.Fatal(err)
257	}
258	if chain != 3 || c.live != 0 {
259		t.Fatal(chain, c.live)
260	}
261}
262
263func TestSyncFromCallback(t *testing.T) {
264	sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
265		if major == 8 {
266			s.event(19, 0)
267			return true
268		}
269		return false
270	}}
271	c, _ := dialScript(t, sc)
272	var errSync, errAtoms, errXID error
273	h := Handler{OnEvent: func(*Event) {
274		errSync = c.Sync(nil)
275		errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1))
276		c.xids, c.xidFreeN = xidRange{}, 0
277		_, errXID = c.NewXID()
278	}}
279	c.Buf().Request(8, 0, 4)
280	if err := c.Sync(&h); err != nil {
281		t.Fatal(err)
282	}
283	if errSync != errReentrantDispatch || errAtoms != errReentrantDispatch || errXID != errReentrantDispatch {
284		t.Fatal(errSync, errAtoms, errXID)
285	}
286}
287
288func TestSyncClosed(t *testing.T) {
289	c, _ := dialScript(t, &script{})
290	_ = c.Close()
291	if err := c.Sync(nil); !errors.Is(err, ErrClosed) {
292		t.Fatal(err)
293	}
294	if err := c.InternAtoms([]string{"A"}, false, make([]uint32, 1)); !errors.Is(err, ErrClosed) {
295		t.Fatal(err)
296	}
297}
298
299func TestNewXIDSetupRange(t *testing.T) {
300	c, _ := dialScript(t, &script{})
301	// Recorded setup: base 0x00200000, mask 0x001fffff.
302	seen := map[uint32]bool{}
303	for range 1000 {
304		id, err := c.NewXID()
305		if err != nil {
306			t.Fatal(err)
307		}
308		if id&^c.xidMask != c.xidBase || seen[id] || len(seen) == 0 && id != c.xidBase {
309			t.Fatalf("id %#x", id)
310		}
311		seen[id] = true
312	}
313	// Freed ids are reused, last freed first; foreign ids are ignored.
314	c.FreeXID(0x00200005)
315	c.FreeXID(0x12345678)
316	c.FreeXID(0)
317	if id, _ := c.NewXID(); id != 0x00200005 {
318		t.Fatalf("%#x", id)
319	}
320	if id, _ := c.NewXID(); seen[id] {
321		t.Fatalf("reused %#x without FreeXID", id)
322	}
323	// The free list is bounded.
324	for i := range xidFreeCap + 10 {
325		c.FreeXID(c.xidBase | uint32(i+1))
326	}
327	if c.xidFreeN != xidFreeCap {
328		t.Fatal(c.xidFreeN)
329	}
330}
331
332func TestNewXIDRefill(t *testing.T) {
333	sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}}
334	c, srv := dialScript(t, sc)
335	// Pretend only two ids of the setup range are left.
336	c.xids.left = 2
337	var ids []uint32
338	for range 5 {
339		id, err := c.NewXID()
340		if err != nil {
341			t.Fatal(len(ids), err)
342		}
343		ids = append(ids, id)
344	}
345	if ids[0] != 0x00200000 || ids[1] != 0x00200001 || ids[2] != 0x00200040 || ids[3] != 0x00200041 || ids[4] != 0x00200042 {
346		t.Fatalf("%#x", ids)
347	}
348	var ranges int
349	for _, r := range srv.requests() {
350		if r.major == 136 && r.minor == minorGetXIDRange {
351			ranges++
352		}
353	}
354	if ranges != 1 {
355		t.Fatal("GetXIDRange requests", ranges)
356	}
357	// The server answers {0, 1}: no ids left.
358	if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) {
359		t.Fatal(err)
360	}
361	// FreeXID still feeds NewXID.
362	c.FreeXID(ids[0])
363	if id, err := c.NewXID(); err != nil || id != ids[0] {
364		t.Fatal(id, err)
365	}
366}
367
368func TestNewXIDNoXCMisc(t *testing.T) {
369	c, _ := dialScript(t, &script{})
370	c.xids = xidRange{}
371	if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) {
372		t.Fatal(err)
373	}
374}
375
376func TestNewXIDRefillError(t *testing.T) {
377	sc := &script{exts: fullExts()}
378	sc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool {
379		if major == 136 && minor == minorGetXIDRange {
380			s.xerror(11, major, 0)
381			return true
382		}
383		return false
384	}
385	c, _ := dialScript(t, sc)
386	c.xids = xidRange{}
387	var xe *XError
388	if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange {
389		t.Fatal(err)
390	}
391}
392
393func TestAllocXID(t *testing.T) {
394	c, _ := dialScript(t, &script{})
395	if n := testing.AllocsPerRun(1000, func() {
396		id, err := c.NewXID()
397		if err != nil {
398			t.Fatal(err)
399		}
400		c.FreeXID(id)
401		if again, _ := c.NewXID(); again != id {
402			t.Fatal("free list")
403		}
404	}); n != 0 {
405		t.Fatalf("NewXID/FreeXID allocate %v times", n)
406	}
407}
408
409// A long silent stream (no replies, no events) gets a GetInputFocus appended
410// by Flush once Next-acked reaches resyncAt; its reply is consumed.
411func TestFlushAutoResync(t *testing.T) {
412	c, srv := dialScript(t, &script{})
413	base := len(srv.requests())
414	start := c.Buf().Next()
415	b := c.Buf()
416	if b.acked != start-1 {
417		t.Fatal("bootstrap not acknowledged", b.acked, start)
418	}
419	// k requests after the last acknowledged one: Next-acked = k+1.
420	for range resyncAt - 2 {
421		b.Request(8, 0, 4) // MapWindow, no reply
422	}
423	if err := flushAll(c); err != nil {
424		t.Fatal(err)
425	}
426	if c.Stats().Resyncs != 0 {
427		t.Fatal("resync below threshold", b.Next()-b.acked)
428	}
429	b.Request(8, 0, 4)
430	if err := flushAll(c); err != nil {
431		t.Fatal(err)
432	}
433	if c.Stats().Resyncs != 1 {
434		t.Fatal(c.Stats())
435	}
436	// Further Flushes do not stack fences while one is in flight.
437	b.Request(8, 0, 4)
438	if err := flushAll(c); err != nil {
439		t.Fatal(err)
440	}
441	if c.Stats().Resyncs != 1 {
442		t.Fatal("second fence while one in flight")
443	}
444	total := int(b.Next()-start) + base
445	waitRequests(t, srv, total)
446	fenceSeq := start + resyncAt - 1
447	fence := srv.requests()[base+resyncAt-1]
448	if fence.major != opGetInputFocus || Seq(fence.seq) != fenceSeq&0xffff {
449		t.Fatalf("fence %+v", fence)
450	}
451	// Its reply acknowledges the stream: no slot left, nothing dropped.
452	dispatchFor(t, c, func() bool { return b.acked >= fenceSeq })
453	if c.live != 0 || c.Stats().DroppedReplies != 0 {
454		t.Fatal(c.live, c.Stats())
455	}
456	// Past the threshold again (relative to the new ack) a new fence goes out.
457	for range resyncAt {
458		b.Request(8, 0, 4)
459	}
460	if err := flushAll(c); err != nil {
461		t.Fatal(err)
462	}
463	if c.Stats().Resyncs != 2 {
464		t.Fatal(c.Stats())
465	}
466}
467
468// flushAll flushes, dispatching (to drain replies) while the socket is full.
469func flushAll(c *Conn) error {
470	for {
471		err := c.Flush()
472		if err != ErrWouldBlock {
473			return err
474		}
475		if err := c.Dispatch(nil); err != nil {
476			return err
477		}
478		time.Sleep(100 * time.Microsecond)
479	}
480}
481
482func dispatchFor(t *testing.T, c *Conn, done func() bool) {
483	t.Helper()
484	deadline := time.Now().Add(5 * time.Second)
485	for !done() {
486		if time.Now().After(deadline) {
487			t.Fatal("timeout")
488		}
489		if err := c.Dispatch(nil); err != nil {
490			t.Fatal(err)
491		}
492		time.Sleep(time.Millisecond)
493	}
494}
495
496// Steady state: Flush with an auto-resync fence does not allocate.
497func TestAllocFlushAutoResync(t *testing.T) {
498	c, srv := dispatchConn(t)
499	sink := make([]byte, 64<<10)
500	b := c.buf
501	b.next, b.acked = 1, 0
502	run := func() {
503		// Pretend the server acknowledged everything so far, then queue
504		// resyncAt requests in one go (no reply ever comes back here).
505		b.acked = b.next - 1
506		c.resync = 0
507		for range resyncAt {
508			b.Request(8, 0, 4)
509		}
510		for c.Flush() == ErrWouldBlock {
511			_, _ = unix.Read(srv, sink)
512		}
513		for {
514			if n, _ := unix.Read(srv, sink); n <= 0 {
515				break
516			}
517		}
518		c.ring[c.resync&ringMask] = slot{}
519		c.live = 0
520	}
521	run()
522	before := c.Stats().Resyncs
523	if n := testing.AllocsPerRun(20, run); n != 0 {
524		t.Fatalf("Flush with resync allocates %v times", n)
525	}
526	if c.Stats().Resyncs-before < 20 {
527		t.Fatal(c.Stats())
528	}
529}

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