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}