blob: 30ec533b2f7b33c5dc1b4e56badabdbf2c4a0905 [file] [log] [blame]
Nigel Tao46544082015-07-29 12:42:58 +10001package xproto
2
3/*
4 Tests for XGB.
5
6 These tests only test the core X protocol at the moment. It isn't even
7 close to complete coverage (and probably never will be), but it does test
8 a number of different corners: requests with no replies, requests without
9 replies, checked (i.e., synchronous) errors, unchecked (i.e., asynchronous)
10 errors, and sequence number wrapping.
11
12 There are also a couple of benchmarks that show the difference between
13 correctly issuing lots of requests and gathering replies and
14 incorrectly doing the same. (This particular difference is one of the
15 claimed advantages of the XCB, and therefore XGB, family.)
16
17 In sum, these tests are more focused on testing the core xgb package itself,
18 rather than whether xproto has properly implemented the core X client
19 protocol.
20*/
21
22import (
23 "fmt"
24 "log"
25 "math/rand"
Brad Fitzpatrickb77eeee2015-07-29 11:52:07 +020026 "os"
Nigel Tao46544082015-07-29 12:42:58 +100027 "testing"
28 "time"
29
30 "github.com/BurntSushi/xgb"
31)
32
33// The X connection used throughout testing.
34var X *xgb.Conn
35
Brad Fitzpatrickb77eeee2015-07-29 11:52:07 +020036func TestMain(m *testing.M) {
37 if os.Getenv("DISPLAY") == "" {
38 log.Printf("No X; Skipping tests")
39 os.Exit(0)
40 }
Nigel Tao46544082015-07-29 12:42:58 +100041
Brad Fitzpatrickb77eeee2015-07-29 11:52:07 +020042 var err error
Nigel Tao46544082015-07-29 12:42:58 +100043 X, err = xgb.NewConn()
44 if err != nil {
45 log.Fatal(err)
46 }
Nigel Tao46544082015-07-29 12:42:58 +100047 rand.Seed(time.Now().UnixNano())
Nigel Tao46544082015-07-29 12:42:58 +100048 go grabEvents()
Brad Fitzpatrickb77eeee2015-07-29 11:52:07 +020049
50 os.Exit(m.Run())
Nigel Tao46544082015-07-29 12:42:58 +100051}
52
53/******************************************************************************/
54// Tests
55/******************************************************************************/
56
57// TestSynchronousError purposefully causes a BadWindow error in a
58// MapWindow request, and checks it synchronously.
59func TestSynchronousError(t *testing.T) {
60 err := MapWindowChecked(X, 0).Check() // resource 0 is always invalid
61 if err == nil {
62 t.Fatalf("MapWindow: A MapWindow request that should return an " +
63 "error has returned a nil error.")
64 }
65 verifyMapWindowError(t, err)
66}
67
68// TestAsynchronousError does the same thing as TestSynchronousError, but
69// grabs the error asynchronously instead.
70func TestAsynchronousError(t *testing.T) {
71 MapWindow(X, 0) // resource id 0 is always invalid
72
73 evOrErr := waitForEvent(t, 5)
74 if evOrErr.ev != nil {
75 t.Fatalf("After issuing an erroneous MapWindow request, we have "+
76 "received an event rather than an error: %s", evOrErr.ev)
77 }
78 verifyMapWindowError(t, evOrErr.err)
79}
80
81// TestCookieBuffer issues (2^16) + n requets *without* replies to guarantee
82// that the sequence number wraps and that the cookie buffer will have to
83// flush itself (since there are no replies coming in to flush it).
84// And just like TestSequenceWrap, we issue another request with a reply
85// at the end to make sure XGB is still working properly.
86func TestCookieBuffer(t *testing.T) {
87 n := (1 << 16) + 10
88 for i := 0; i < n; i++ {
89 NoOperation(X)
90 }
91 TestProperty(t)
92}
93
94// TestSequenceWrap issues (2^16) + n requests w/ replies to guarantee that the
95// sequence number (which is a 16 bit integer) will wrap. It then issues one
96// final request to ensure things still work properly.
97func TestSequenceWrap(t *testing.T) {
98 n := (1 << 16) + 10
99 for i := 0; i < n; i++ {
100 _, err := InternAtom(X, false, 5, "RANDO").Reply()
101 if err != nil {
102 t.Fatalf("InternAtom: %s", err)
103 }
104 }
105 TestProperty(t)
106}
107
108// TestProperty tests whether a random value can be set and read.
109func TestProperty(t *testing.T) {
110 propName := randString(20) // whatevs
111 writeVal := randString(20)
112 readVal, err := changeAndGetProp(propName, writeVal)
113 if err != nil {
114 t.Error(err)
115 }
116
117 if readVal != writeVal {
118 t.Errorf("The value written, '%s', is not the same as the "+
119 "value read '%s'.", writeVal, readVal)
120 }
121}
122
123// TestWindowEvents creates a window, maps it, listens for configure notify
124// events, issues a configure request, and checks for the appropriate
125// configure notify event.
126// This probably violates the notion of "test one thing and test it well,"
127// but testing X stuff is unique since it involves so much state.
128// Each request is checked to make sure there are no errors returned. If there
129// is an error, the test is failed.
130// You may see a window appear quickly and then disappear. Do not be alarmed :P
131// It's possible that this test will yield a false negative because we cannot
132// control our environment. That is, the window manager could override the
133// placement set. However, we set override redirect on the window, so the
134// window manager *shouldn't* touch our window if it is well-behaved.
135func TestWindowEvents(t *testing.T) {
136 // The geometry to set the window.
137 gx, gy, gw, gh := 200, 400, 1000, 300
138
139 wid, err := NewWindowId(X)
140 if err != nil {
141 t.Fatalf("NewId: %s", err)
142 }
143
144 screen := Setup(X).DefaultScreen(X) // alias
145 err = CreateWindowChecked(X, screen.RootDepth, wid, screen.Root,
146 0, 0, 500, 500, 0,
147 WindowClassInputOutput, screen.RootVisual,
148 CwBackPixel|CwOverrideRedirect, []uint32{0xffffffff, 1}).Check()
149 if err != nil {
150 t.Fatalf("CreateWindow: %s", err)
151 }
152
153 err = MapWindowChecked(X, wid).Check()
154 if err != nil {
155 t.Fatalf("MapWindow: %s", err)
156 }
157
158 // We don't listen in the CreateWindow request so that we don't get
159 // a MapNotify event.
160 err = ChangeWindowAttributesChecked(X, wid,
161 CwEventMask, []uint32{EventMaskStructureNotify}).Check()
162 if err != nil {
163 t.Fatalf("ChangeWindowAttributes: %s", err)
164 }
165
166 err = ConfigureWindowChecked(X, wid,
167 ConfigWindowX|ConfigWindowY|
168 ConfigWindowWidth|ConfigWindowHeight,
169 []uint32{uint32(gx), uint32(gy), uint32(gw), uint32(gh)}).Check()
170 if err != nil {
171 t.Fatalf("ConfigureWindow: %s", err)
172 }
173
174 evOrErr := waitForEvent(t, 5)
175 switch event := evOrErr.ev.(type) {
176 case ConfigureNotifyEvent:
177 if event.X != int16(gx) {
178 t.Fatalf("x was set to %d but ConfigureNotify reports %d",
179 gx, event.X)
180 }
181 if event.Y != int16(gy) {
182 t.Fatalf("y was set to %d but ConfigureNotify reports %d",
183 gy, event.Y)
184 }
185 if event.Width != uint16(gw) {
186 t.Fatalf("width was set to %d but ConfigureNotify reports %d",
187 gw, event.Width)
188 }
189 if event.Height != uint16(gh) {
190 t.Fatalf("height was set to %d but ConfigureNotify reports %d",
191 gh, event.Height)
192 }
193 default:
194 t.Fatalf("Expected a ConfigureNotifyEvent but got %T instead.", event)
195 }
196
197 // Okay, clean up!
198 err = ChangeWindowAttributesChecked(X, wid,
199 CwEventMask, []uint32{0}).Check()
200 if err != nil {
201 t.Fatalf("ChangeWindowAttributes: %s", err)
202 }
203
204 err = DestroyWindowChecked(X, wid).Check()
205 if err != nil {
206 t.Fatalf("DestroyWindow: %s", err)
207 }
208}
209
210// Calls GetFontPath function, Issue #12
211func TestGetFontPath(t *testing.T) {
212 fontPathReply, err := GetFontPath(X).Reply()
213 if err != nil {
214 t.Fatalf("GetFontPath: %v", err)
215 }
216 _ = fontPathReply
217}
218
219func TestListFonts(t *testing.T) {
220 listFontsReply, err := ListFonts(X, 10, 1, "*").Reply()
221 if err != nil {
222 t.Fatalf("ListFonts: %v", err)
223 }
224 _ = listFontsReply
225}
226
227/******************************************************************************/
228// Benchmarks
229/******************************************************************************/
230
231// BenchmarkInternAtomsGood shows how many requests with replies
232// *should* be sent and gathered from the server. Namely, send as many
233// requests as you can at once, then go back and gather up all the replies.
234// More importantly, this approach can exploit parallelism when
235// GOMAXPROCS > 1.
236// Run with `go test -run 'nomatch' -bench '.*' -cpu 1,2,6` if you have
237// multiple cores to see the improvement that parallelism brings.
238func BenchmarkInternAtomsGood(b *testing.B) {
239 b.StopTimer()
240 names := seqNames(b.N)
241
242 b.StartTimer()
243 cookies := make([]InternAtomCookie, b.N)
244 for i := 0; i < b.N; i++ {
245 cookies[i] = InternAtom(X, false, uint16(len(names[i])), names[i])
246 }
247 for _, cookie := range cookies {
248 cookie.Reply()
249 }
250}
251
252// BenchmarkInternAtomsBad shows how *not* to issue a lot of requests with
253// replies. Namely, each subsequent request isn't issued *until* the last
254// reply is made. This implies a round trip to the X server for every
255// iteration.
256func BenchmarkInternAtomsPoor(b *testing.B) {
257 b.StopTimer()
258 names := seqNames(b.N)
259
260 b.StartTimer()
261 for i := 0; i < b.N; i++ {
262 InternAtom(X, false, uint16(len(names[i])), names[i]).Reply()
263 }
264}
265
266/******************************************************************************/
267// Helper functions
268/******************************************************************************/
269
270// changeAndGetProp sets property 'prop' with value 'val'.
271// It then gets the value of that property and returns it.
272// (It's used to check that the 'val' going in is the same 'val' going out.)
273// It tests both requests with and without replies (GetProperty and
274// ChangeProperty respectively.)
275func changeAndGetProp(prop, val string) (string, error) {
276 setup := Setup(X)
277 root := setup.DefaultScreen(X).Root
278
279 propAtom, err := InternAtom(X, false, uint16(len(prop)), prop).Reply()
280 if err != nil {
281 return "", fmt.Errorf("InternAtom: %s", err)
282 }
283
284 typName := "UTF8_STRING"
285 typAtom, err := InternAtom(X, false, uint16(len(typName)), typName).Reply()
286 if err != nil {
287 return "", fmt.Errorf("InternAtom: %s", err)
288 }
289
290 err = ChangePropertyChecked(X, PropModeReplace, root, propAtom.Atom,
291 typAtom.Atom, 8, uint32(len(val)), []byte(val)).Check()
292 if err != nil {
293 return "", fmt.Errorf("ChangeProperty: %s", err)
294 }
295
296 reply, err := GetProperty(X, false, root, propAtom.Atom,
297 GetPropertyTypeAny, 0, (1<<32)-1).Reply()
298 if err != nil {
299 return "", fmt.Errorf("GetProperty: %s", err)
300 }
301 if reply.Format != 8 {
302 return "", fmt.Errorf("Property reply format is %d but it should be 8.",
303 reply.Format)
304 }
305
306 return string(reply.Value), nil
307}
308
309// verifyMapWindowError takes an error that is returned with an invalid
310// MapWindow request with a window Id of 0 and makes sure the error is the
311// right type and contains the correct values.
312func verifyMapWindowError(t *testing.T, err error) {
313 switch e := err.(type) {
314 case WindowError:
315 if e.BadValue != 0 {
316 t.Fatalf("WindowError should report a bad value of 0 but "+
317 "it reports %d instead.", e.BadValue)
318 }
319 if e.MajorOpcode != 8 {
320 t.Fatalf("WindowError should report a major opcode of 8 "+
321 "(which is a MapWindow request), but it reports %d instead.",
322 e.MajorOpcode)
323 }
324 default:
325 t.Fatalf("Expected a WindowError but got %T instead.", e)
326 }
327}
328
329// randString generates a random string of length n.
330func randString(n int) string {
331 byts := make([]byte, n)
332 for i := 0; i < n; i++ {
333 rando := rand.Intn(53)
334 switch {
335 case rando <= 25:
336 byts[i] = byte(65 + rando)
337 case rando <= 51:
338 byts[i] = byte(97 + rando - 26)
339 default:
340 byts[i] = ' '
341 }
342 }
343 return string(byts)
344}
345
346// seqNames creates a slice of NAME0, NAME1, ..., NAMEN.
347func seqNames(n int) []string {
348 names := make([]string, n)
349 for i := range names {
350 names[i] = fmt.Sprintf("NAME%d", i)
351 }
352 return names
353}
354
355// evErr represents a value that is either an event or an error.
356type evErr struct {
357 ev xgb.Event
358 err xgb.Error
359}
360
361// channel used to pass evErrs.
362var evOrErrChan = make(chan evErr, 0)
363
364// grabEvents is a goroutine that reads events off the wire.
365// We used this instead of WaitForEvent directly in our tests so that
366// we can timeout and fail a test.
367func grabEvents() {
368 for {
369 ev, err := X.WaitForEvent()
370 evOrErrChan <- evErr{ev, err}
371 }
372}
373
374// waitForEvent asks the evOrErrChan channel for an event.
375// If it doesn't get an event in 'n' seconds, the current test is failed.
376func waitForEvent(t *testing.T, n int) evErr {
377 var evOrErr evErr
378
379 select {
380 case evOrErr = <-evOrErrChan:
381 case <-time.After(time.Second * 5):
382 t.Fatalf("After waiting 5 seconds for an event or an error, " +
383 "we have timed out.")
384 }
385
386 return evOrErr
387}