package xturbo import ( "encoding/binary" "errors" "testing" "time" "golang.org/x/sys/unix" ) // waitRequests waits until the scripted server has read n requests. func waitRequests(t *testing.T, srv *scriptConn, n int) { t.Helper() deadline := time.Now().Add(5 * time.Second) for len(srv.requests()) < n { if time.Now().After(deadline) { t.Fatalf("server saw %d requests, want %d", len(srv.requests()), n) } time.Sleep(time.Millisecond) } } func TestBootstrapAllExtensions(t *testing.T) { exts := fullExts() c, srv := dialScript(t, &script{exts: exts, bigreqMax: 0x3fffff}) for id := ExtCore + 1; id < extCount; id++ { e := exts[ExtXName[id]] want := ExtInfo{true, e.major, e.firstEvent, e.firstError} if got := c.Ext().Info(id); got != want { t.Errorf("%s: %+v, want %+v", ExtXName[id], got, want) } } if id, rel, ok := c.Ext().ByEvent(88); !ok || id != ExtXFixes || rel != 1 { t.Fatal("reverse event map", id, rel, ok) } if c.buf.maxReq != 0x3fffff*4 { t.Fatalf("maxReq %d", c.buf.maxReq) } // One QueryExtension per extension, then Enable (queued once // BIG-REQUESTS answered), fences; nothing left registered. reqs := srv.requests() for i := range int(extCount) - 1 { if reqs[i].major != opQueryExtension { t.Fatalf("request %d major %d", i, reqs[i].major) } } var enables int for _, r := range reqs { if r.major == 133 { enables++ } } if enables != 1 || c.live != 0 || c.Stats().DroppedReplies != 0 { t.Fatalf("enables %d live %d %+v", enables, c.live, c.Stats()) } // A big request now encodes with the 8-byte header. if p, _ := c.Buf().Request(18, 0, coreMaxReq); p == nil { t.Fatal(c.Buf().Err()) } } func TestBootstrapAbsentExtensions(t *testing.T) { exts := fullExts() delete(exts, "BIG-REQUESTS") delete(exts, "Composite") c, srv := dialScript(t, &script{exts: exts}) if c.Ext().Info(ExtComposite).Present || c.Ext().Major(ExtBigRequests) != 0 { t.Fatal("absent extension marked present") } if !c.Ext().Info(ExtXFixes).Present { t.Fatal("XFIXES missing") } if c.buf.maxReq != coreMaxReq { t.Fatalf("maxReq %d without BIG-REQUESTS", c.buf.maxReq) } for _, r := range srv.requests() { if r.major >= 128 { t.Fatalf("extension request %d sent", r.major) } } if p, _ := c.Buf().Request(18, 0, coreMaxReq); p != nil || !errors.Is(c.Buf().Err(), ErrRequestTooLarge) { t.Fatal("big request without BIG-REQUESTS") } } func TestBootstrapErrorFailsConnect(t *testing.T) { fd, _ := startScript(t, &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { if major == opQueryExtension { s.xerror(11, major, 0) return true } return false }}) c, err := NewConn(fd) var xe *XError if c != nil || !errors.As(err, &xe) || xe.Kind != kindQueryExtension || xe.Code != 11 { t.Fatalf("conn %v err %v", c != nil, err) } } func TestInternAtoms(t *testing.T) { sc := &script{atoms: map[string]uint32{"WM_STATE": 300, "_NET_WM_NAME": 301}} c, srv := dialScript(t, sc) before := len(srv.requests()) names := []string{"_NET_WM_NAME", "NEW_ONE", "WM_STATE", "NEW_TWO"} dst := make([]uint32, len(names)) if err := c.InternAtoms(names, false, dst); err != nil { t.Fatal(err) } if dst[0] != 301 || dst[2] != 300 || dst[1] == 0 || dst[3] == 0 || dst[1] == dst[3] { t.Fatal(dst) } if dst[1] != srv.atom("NEW_ONE") || dst[3] != srv.atom("NEW_TWO") { t.Fatal("atoms out of order", dst) } // Pipelined in name order, then one fence. reqs := srv.requests()[before:] if len(reqs) != len(names)+1 || reqs[len(names)].major != opGetInputFocus { t.Fatalf("%d requests", len(reqs)) } for i, r := range reqs[:len(names)] { if r.major != opInternAtom || string(r.raw[8:8+len(names[i])]) != names[i] { t.Fatalf("request %d: %x", i, r.raw) } } // onlyIfExists: unknown names give 0. dst2 := make([]uint32, 2) if err := c.InternAtoms([]string{"WM_STATE", "NOPE"}, true, dst2); err != nil || dst2[0] != 300 || dst2[1] != 0 { t.Fatal(err, dst2) } if err := c.InternAtoms(names, false, dst[:1]); !errors.Is(err, ErrBadLength) { t.Fatal(err) } if err := c.InternAtoms(nil, false, nil); err != nil { t.Fatal(err) } } func TestInternAtomsError(t *testing.T) { sc := &script{badAtoms: map[string]bool{"BAD": true}} c, _ := dialScript(t, sc) var userErrs int h := Handler{OnError: func(*XError) { userErrs++ }} if err := c.Dispatch(&h); err != nil { t.Fatal(err) } dst := make([]uint32, 3) err := c.InternAtoms([]string{"A", "BAD", "C"}, false, dst) var xe *XError if !errors.As(err, &xe) || xe.Code != 11 || xe.Args[0] != 1 || xe.Kind != kindInternAtom || xe.Major != opInternAtom { t.Fatalf("%v", err) } if dst[0] == 0 || dst[1] != 0 || dst[2] == 0 || userErrs != 0 { t.Fatal(dst, userErrs) } // The error does not stick to the next call. if err := c.InternAtoms([]string{"D"}, false, dst[:1]); err != nil { t.Fatal(err) } } func TestInternAtomsManyWrapsRing(t *testing.T) { c, _ := dialScript(t, &script{}) names := make([]string, 3*ringSize+7) for i := range names { names[i] = "ATOM_" + string(rune('A'+i%26)) + string(rune('a'+i/26%26)) + string(rune('0'+i/676)) } dst := make([]uint32, len(names)) if err := c.InternAtoms(names, false, dst); err != nil { t.Fatal(err) } seen := map[uint32]bool{} for i, a := range dst { if a == 0 || seen[a] { t.Fatalf("atom %d = %d", i, a) } seen[a] = true } } // Sync delivers user packets that arrive meanwhile to h, waits for user // registrations and keeps errors on reserved requests away from OnError. func TestSyncInterleavedEvents(t *testing.T) { const kindProbe = kindUser + 3 sc := &script{} sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { switch major { case 8: // MapWindow: emit a MapNotify and a MapRequest s.event(19, 1) s.event(20, 2) return true case 14: // GetGeometry: answer late, after an event s.event(28, 3) s.reply(24, make([]byte, 16)) return true case 10: // UnmapWindow: user error s.xerror(3, major, 0x42) return true } return false } c, _ := dialScript(t, sc) var events []EventKind var replies []uint16 var errs []uint8 h := Handler{ OnEvent: func(ev *Event) { events = append(events, ev.Kind) }, OnReply: func(r *Reply) { replies = append(replies, r.Kind) }, OnError: func(e *XError) { errs = append(errs, e.Code) }, } b := c.Buf() p, _ := b.Request(8, 0, 4) binary.LittleEndian.PutUint32(p, 7) _, geom := b.Request(14, 0, 4) if err := c.Expect(geom, kindProbe, 0, 0, 0, 0); err != nil { t.Fatal(err) } b.Request(10, 0, 4) if err := c.Sync(&h); err != nil { t.Fatal(err) } if len(events) != 3 || events[0] != 19 || events[1] != 20 || events[2] != 28 { t.Fatal(events) } if len(replies) != 1 || replies[0] != kindProbe || len(errs) != 1 || errs[0] != 3 || c.live != 0 { t.Fatal(replies, errs, c.live) } } // A callback registering more requests during Sync gets them waited for too. func TestSyncFollowsCallbackRequests(t *testing.T) { const kindChain = kindUser + 4 sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { if major == 14 { s.reply(0, nil) return true } return false }} c, _ := dialScript(t, sc) chain := 0 var h Handler h.OnReply = func(r *Reply) { chain++ if chain < 3 { _, seq := c.Buf().Request(14, 0, 4) if err := c.Expect(seq, kindChain, 0, 0, 0, 0); err != nil { t.Error(err) } } } _, seq := c.Buf().Request(14, 0, 4) _ = c.Expect(seq, kindChain, 0, 0, 0, 0) if err := c.Sync(&h); err != nil { t.Fatal(err) } if chain != 3 || c.live != 0 { t.Fatal(chain, c.live) } } func TestSyncFromCallback(t *testing.T) { sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { if major == 8 { s.event(19, 0) return true } return false }} c, _ := dialScript(t, sc) var errSync, errAtoms, errXID error h := Handler{OnEvent: func(*Event) { errSync = c.Sync(nil) errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1)) c.xids, c.xidFreeN = xidRange{}, 0 _, errXID = c.NewXID() }} c.Buf().Request(8, 0, 4) if err := c.Sync(&h); err != nil { t.Fatal(err) } if errSync != errReentrantDispatch || errAtoms != errReentrantDispatch || errXID != errReentrantDispatch { t.Fatal(errSync, errAtoms, errXID) } } func TestSyncClosed(t *testing.T) { c, _ := dialScript(t, &script{}) _ = c.Close() if err := c.Sync(nil); !errors.Is(err, ErrClosed) { t.Fatal(err) } if err := c.InternAtoms([]string{"A"}, false, make([]uint32, 1)); !errors.Is(err, ErrClosed) { t.Fatal(err) } } func TestNewXIDSetupRange(t *testing.T) { c, _ := dialScript(t, &script{}) // Recorded setup: base 0x00200000, mask 0x001fffff. seen := map[uint32]bool{} for range 1000 { id, err := c.NewXID() if err != nil { t.Fatal(err) } if id&^c.xidMask != c.xidBase || seen[id] || len(seen) == 0 && id != c.xidBase { t.Fatalf("id %#x", id) } seen[id] = true } // Freed ids are reused, last freed first; foreign ids are ignored. c.FreeXID(0x00200005) c.FreeXID(0x12345678) c.FreeXID(0) if id, _ := c.NewXID(); id != 0x00200005 { t.Fatalf("%#x", id) } if id, _ := c.NewXID(); seen[id] { t.Fatalf("reused %#x without FreeXID", id) } // The free list is bounded. for i := range xidFreeCap + 10 { c.FreeXID(c.xidBase | uint32(i+1)) } if c.xidFreeN != xidFreeCap { t.Fatal(c.xidFreeN) } } func TestNewXIDRefill(t *testing.T) { sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}} c, srv := dialScript(t, sc) // Pretend only two ids of the setup range are left. c.xids.left = 2 var ids []uint32 for range 5 { id, err := c.NewXID() if err != nil { t.Fatal(len(ids), err) } ids = append(ids, id) } if ids[0] != 0x00200000 || ids[1] != 0x00200001 || ids[2] != 0x00200040 || ids[3] != 0x00200041 || ids[4] != 0x00200042 { t.Fatalf("%#x", ids) } var ranges int for _, r := range srv.requests() { if r.major == 136 && r.minor == minorGetXIDRange { ranges++ } } if ranges != 1 { t.Fatal("GetXIDRange requests", ranges) } // The server answers {0, 1}: no ids left. if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { t.Fatal(err) } // FreeXID still feeds NewXID. c.FreeXID(ids[0]) if id, err := c.NewXID(); err != nil || id != ids[0] { t.Fatal(id, err) } } func TestNewXIDNoXCMisc(t *testing.T) { c, _ := dialScript(t, &script{}) c.xids = xidRange{} if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) { t.Fatal(err) } } func TestNewXIDRefillError(t *testing.T) { sc := &script{exts: fullExts()} sc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { if major == 136 && minor == minorGetXIDRange { s.xerror(11, major, 0) return true } return false } c, _ := dialScript(t, sc) c.xids = xidRange{} var xe *XError if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange { t.Fatal(err) } } func TestAllocXID(t *testing.T) { c, _ := dialScript(t, &script{}) if n := testing.AllocsPerRun(1000, func() { id, err := c.NewXID() if err != nil { t.Fatal(err) } c.FreeXID(id) if again, _ := c.NewXID(); again != id { t.Fatal("free list") } }); n != 0 { t.Fatalf("NewXID/FreeXID allocate %v times", n) } } // A long silent stream (no replies, no events) gets a GetInputFocus appended // by Flush once Next-acked reaches resyncAt; its reply is consumed. func TestFlushAutoResync(t *testing.T) { c, srv := dialScript(t, &script{}) base := len(srv.requests()) start := c.Buf().Next() b := c.Buf() if b.acked != start-1 { t.Fatal("bootstrap not acknowledged", b.acked, start) } // k requests after the last acknowledged one: Next-acked = k+1. for range resyncAt - 2 { b.Request(8, 0, 4) // MapWindow, no reply } if err := flushAll(c); err != nil { t.Fatal(err) } if c.Stats().Resyncs != 0 { t.Fatal("resync below threshold", b.Next()-b.acked) } b.Request(8, 0, 4) if err := flushAll(c); err != nil { t.Fatal(err) } if c.Stats().Resyncs != 1 { t.Fatal(c.Stats()) } // Further Flushes do not stack fences while one is in flight. b.Request(8, 0, 4) if err := flushAll(c); err != nil { t.Fatal(err) } if c.Stats().Resyncs != 1 { t.Fatal("second fence while one in flight") } total := int(b.Next()-start) + base waitRequests(t, srv, total) fenceSeq := start + resyncAt - 1 fence := srv.requests()[base+resyncAt-1] if fence.major != opGetInputFocus || Seq(fence.seq) != fenceSeq&0xffff { t.Fatalf("fence %+v", fence) } // Its reply acknowledges the stream: no slot left, nothing dropped. dispatchFor(t, c, func() bool { return b.acked >= fenceSeq }) if c.live != 0 || c.Stats().DroppedReplies != 0 { t.Fatal(c.live, c.Stats()) } // Past the threshold again (relative to the new ack) a new fence goes out. for range resyncAt { b.Request(8, 0, 4) } if err := flushAll(c); err != nil { t.Fatal(err) } if c.Stats().Resyncs != 2 { t.Fatal(c.Stats()) } } // flushAll flushes, dispatching (to drain replies) while the socket is full. func flushAll(c *Conn) error { for { err := c.Flush() if err != ErrWouldBlock { return err } if err := c.Dispatch(nil); err != nil { return err } time.Sleep(100 * time.Microsecond) } } func dispatchFor(t *testing.T, c *Conn, done func() bool) { t.Helper() deadline := time.Now().Add(5 * time.Second) for !done() { if time.Now().After(deadline) { t.Fatal("timeout") } if err := c.Dispatch(nil); err != nil { t.Fatal(err) } time.Sleep(time.Millisecond) } } // Steady state: Flush with an auto-resync fence does not allocate. func TestAllocFlushAutoResync(t *testing.T) { c, srv := dispatchConn(t) sink := make([]byte, 64<<10) b := c.buf b.next, b.acked = 1, 0 run := func() { // Pretend the server acknowledged everything so far, then queue // resyncAt requests in one go (no reply ever comes back here). b.acked = b.next - 1 c.resync = 0 for range resyncAt { b.Request(8, 0, 4) } for c.Flush() == ErrWouldBlock { _, _ = unix.Read(srv, sink) } for { if n, _ := unix.Read(srv, sink); n <= 0 { break } } c.ring[c.resync&ringMask] = slot{} c.live = 0 } run() before := c.Stats().Resyncs if n := testing.AllocsPerRun(20, run); n != 0 { t.Fatalf("Flush with resync allocates %v times", n) } if c.Stats().Resyncs-before < 20 { t.Fatal(c.Stats()) } }