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