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}