//go:build xserver package xturbo_test import ( "crypto/rand" "errors" "path/filepath" "testing" "bnema.dev/xturbo" "bnema.dev/xturbo/xproto" ) func dialXvfb(t *testing.T) *xturbo.Conn { t.Helper() dir := t.TempDir() auth := filepath.Join(dir, "Xauthority") cookie := make([]byte, 16) _, _ = rand.Read(cookie) writeXauthority(t, auth, cookie) display := startXvfb(t, auth) t.Setenv("XAUTHORITY", auth) c, err := xturbo.Dial(display) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = c.Close() }) return c } // The ExtTable filled at connect matches what independent QueryExtension // round trips through the generated encoder report. func TestXserverExtTable(t *testing.T) { c := dialXvfb(t) const kindQuery = 16 type got struct { present bool major, firstEv, firstErr uint8 } answers := map[uint32]got{} h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { q, ok := xproto.ParseQueryExtensionReply(r.Raw) if !ok { t.Error("short QueryExtension reply") return } answers[r.Args[0]] = got{q.Present(), q.MajorOpcode(), q.FirstEvent(), q.FirstError()} }} for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { seq := xproto.QueryExtension(c.Buf(), xturbo.ExtXName[id]) if err := c.Expect(seq, kindQuery, uint32(id), 0, 0, 0); err != nil { t.Fatal(err) } } if err := c.Sync(&h); err != nil { t.Fatal(err) } for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { a, ok := answers[uint32(id)] info := c.Ext().Info(id) if !ok { t.Fatalf("%s: no reply", xturbo.ExtXName[id]) } want := xturbo.ExtInfo{} if a.present { want = xturbo.ExtInfo{Present: true, Major: a.major, FirstEvent: a.firstEv, FirstError: a.firstErr} } if info != want { t.Errorf("%s: table %+v, server %+v", xturbo.ExtXName[id], info, a) } } // Xvfb builds XFIXES, Composite, RENDER, SHAPE, X-Resource, XC-MISC and // BIG-REQUESTS in; the XWM depends on them. for _, id := range []xturbo.ExtID{xturbo.ExtXFixes, xturbo.ExtComposite, xturbo.ExtRender, xturbo.ExtShape, xturbo.ExtRes, xturbo.ExtXCMisc, xturbo.ExtBigRequests} { if !c.Ext().Info(id).Present { t.Errorf("%s absent", xturbo.ExtXName[id]) } } if st := c.Stats(); st.DroppedReplies != 0 { t.Fatal(st) } } // InternAtoms agrees with independent InternAtom round trips and with the // predefined atoms; errors come back as *XError. func TestXserverInternAtoms(t *testing.T) { c := dialXvfb(t) names := []string{"WM_NAME", "STRING", "WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING", "_NET_SUPPORTED", "WM_STATE", "XTURBO_TEST_ATOM"} dst := make([]uint32, len(names)) if err := c.InternAtoms(names, false, dst); err != nil { t.Fatal(err) } if dst[0] != uint32(xproto.AtomWmName) || dst[1] != uint32(xproto.AtomString) { t.Fatalf("predefined atoms %v", dst[:2]) } const kindAtom = 16 again := make([]uint32, len(names)) h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { a, ok := xproto.ParseInternAtomReply(r.Raw) if ok { again[r.Args[0]] = uint32(a.Atom()) } }} for i, name := range names { seq := xproto.InternAtom(c.Buf(), true, name) if err := c.Expect(seq, kindAtom, uint32(i), 0, 0, 0); err != nil { t.Fatal(err) } } if err := c.Sync(&h); err != nil { t.Fatal(err) } seen := map[uint32]bool{} for i := range names { if dst[i] == 0 || dst[i] != again[i] || seen[dst[i]] { t.Fatalf("%s: %d vs %d", names[i], dst[i], again[i]) } seen[dst[i]] = true } // onlyIfExists on an unknown name gives 0, no error. none := make([]uint32, 1) if err := c.InternAtoms([]string{"XTURBO_NEVER_INTERNED_ATOM"}, true, none); err != nil || none[0] != 0 { t.Fatal(err, none) } } // NewXID hands out many distinct ids in the client's range; windows created // with them exist and can be destroyed and the ids reused. func TestXserverNewXIDWindows(t *testing.T) { c := dialXvfb(t) const n = 2000 ids := make([]uint32, 0, n) seen := map[uint32]bool{} for range n { id, err := c.NewXID() if err != nil { t.Fatal(err) } if id == 0 || seen[id] { t.Fatalf("id %#x", id) } seen[id] = true ids = append(ids, id) } b := c.Buf() var v xproto.CreateWindowValues for _, id := range ids[:64] { xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 1, 2, 30, 40, 0, xproto.WindowClassInputOutput, 0, &v) } var xerrs []xturbo.XError geoms := 0 const kindGeom = 16 h := xturbo.Handler{ OnReply: func(r *xturbo.Reply) { if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok && g.Width() == 30 && g.Height() == 40 { geoms++ } }, OnError: func(e *xturbo.XError) { xerrs = append(xerrs, *e) }, } for _, id := range ids[:64] { seq := xproto.GetGeometry(b, xproto.Drawable(id)) if err := c.Expect(seq, kindGeom, id, 0, 0, 0); err != nil { t.Fatal(err) } } if err := c.Sync(&h); err != nil { t.Fatal(err) } if geoms != 64 || len(xerrs) != 0 { t.Fatalf("geometries %d errors %v", geoms, xerrs) } for _, id := range ids[:64] { xproto.DestroyWindow(b, xproto.Window(id)) c.FreeXID(id) } if err := c.Sync(&h); err != nil || len(xerrs) != 0 { t.Fatal(err, xerrs) } // A reused id can be created again: the destroy completed. id, err := c.NewXID() if err != nil || !seen[id] { t.Fatalf("reuse %#x %v", id, err) } xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) xproto.DestroyWindow(b, xproto.Window(id)) if err := c.Sync(&h); err != nil || len(xerrs) != 0 { t.Fatal(err, xerrs) } } // Exhausting the setup range triggers an XC-MISC GetXIDRange refill; the // refilled ids are usable. func TestXserverXIDRefill(t *testing.T) { c := dialXvfb(t) xturbo.ExhaustXIDs(c) id, err := c.NewXID() if err != nil { t.Fatal(err) } var v xproto.CreateWindowValues xproto.CreateWindow(c.Buf(), 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) xproto.DestroyWindow(c.Buf(), xproto.Window(id)) var xerr *xturbo.XError h := xturbo.Handler{OnError: func(e *xturbo.XError) { cp := *e; xerr = &cp }} if err := c.Sync(&h); err != nil || xerr != nil { t.Fatal(err, xerr) } // Non-blocking prefetch: queued from the loop, used after the next // exhaustion without a blocking round trip. xturbo.ExhaustXIDs(c) if !c.XIDsLow() { t.Fatal("not low after exhaustion") } if err := c.PrefetchXIDs(); err != nil { t.Fatal(err) } if err := c.Sync(&h); err != nil || c.XIDsLow() { t.Fatal(err, c.XIDsLow()) } // The server hands out its lowest free range, which may hold the id just // destroyed: check the id is usable, not that it is new. id2, err := c.NewXID() if err != nil { t.Fatal(err) } xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) xproto.DestroyWindow(c.Buf(), xproto.Window(id2)) if err := c.Sync(&h); err != nil || xerr != nil { t.Fatal(err, xerr) } } // A long silent stream gets fences; the connection keeps working. func TestXserverAutoResync(t *testing.T) { c := dialXvfb(t) b := c.Buf() for range 3 * 0x8000 { xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event if b.Len() > 32<<10 { if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) { t.Fatal(err) } if err := c.Dispatch(nil); err != nil { t.Fatal(err) } } } if b.Err() != nil { t.Fatal(b.Err()) } if err := c.Sync(nil); err != nil { t.Fatal(err) } if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 { t.Fatalf("%+v", st) } }