aboutsummaryrefslogtreecommitdiff
path: root/nexgb/xgb_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'nexgb/xgb_test.go')
-rw-r--r--nexgb/xgb_test.go358
1 files changed, 358 insertions, 0 deletions
diff --git a/nexgb/xgb_test.go b/nexgb/xgb_test.go
new file mode 100644
index 0000000..9665164
--- /dev/null
+++ b/nexgb/xgb_test.go
@@ -0,0 +1,358 @@
+package xgb
+
+/*
+ Tests for XGB.
+
+ These tests only test the core X protocol at the moment. It isn't even
+ close to complete coverage (and probably never will be), but it does test
+ a number of different corners: requests with no replies, requests without
+ replies, checked (i.e., synchronous) errors, unchecked (i.e., asynchronous)
+ errors, and sequence number wrapping.
+
+ There are also a couple of benchmarks that show the difference between
+ correctly issuing lots of requests and gathering replies and
+ incorrectly doing the same. (This particular difference is one of the
+ claimed advantages of the XCB, and therefore XGB, family.
+*/
+
+import (
+ "fmt"
+ "log"
+ "math/rand"
+ "testing"
+ "time"
+)
+
+// The X connection used throughout testing.
+var X *Conn
+
+// init initializes the X connection, seeds the RNG and starts waiting
+// for events.
+func init() {
+ var err error
+
+ X, err = NewConn()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ rand.Seed(time.Now().UnixNano())
+
+ go grabEvents()
+}
+
+/******************************************************************************/
+// Tests
+/******************************************************************************/
+
+// TestSynchronousError purposefully causes a BadLength error in an
+// InternAtom request, and checks it synchronously.
+func TestSynchronousError(t *testing.T) {
+ err := X.MapWindowChecked(0).Check() // resource id 0 is always invalid
+ if err == nil {
+ t.Fatalf("MapWindow: A MapWindow request that should return an " +
+ "error has returned a nil error.")
+ }
+ verifyMapWindowError(t, err)
+}
+
+// TestAsynchronousError does the same thing as TestSynchronousError, but
+// grabs the error asynchronously instead.
+func TestAsynchronousError(t *testing.T) {
+ X.MapWindow(0) // resource id 0 is always invalid
+
+ evOrErr := waitForEvent(t, 5)
+ if evOrErr.ev != nil {
+ t.Fatalf("After issuing an erroneous MapWindow request, we have "+
+ "received an event rather than an error: %s", evOrErr.ev)
+ }
+ verifyMapWindowError(t, evOrErr.err)
+}
+
+// TestCookieBuffer issues (2^16) + n requets *without* replies to guarantee
+// that the sequence number wraps and that the cookie buffer will have to
+// flush itself (since there are no replies coming in to flush it).
+// And just like TestSequenceWrap, we issue another request with a reply
+// at the end to make sure XGB is still working properly.
+func TestCookieBuffer(t *testing.T) {
+ n := (1 << 16) + 10
+ for i := 0; i < n; i++ {
+ X.NoOperation()
+ }
+ TestProperty(t)
+}
+
+// TestSequenceWrap issues (2^16) + n requests w/ replies to guarantee that the
+// sequence number (which is a 16 bit integer) will wrap. It then issues one
+// final request to ensure things still work properly.
+func TestSequenceWrap(t *testing.T) {
+ n := (1 << 16) + 10
+ for i := 0; i < n; i++ {
+ _, err := X.InternAtom(false, 5, "RANDO").Reply()
+ if err != nil {
+ t.Fatalf("InternAtom: %s", err)
+ }
+ }
+ TestProperty(t)
+}
+
+// TestProperty tests whether a random value can be set and read.
+func TestProperty(t *testing.T) {
+ propName := randString(20) // whatevs
+ writeVal := randString(20)
+ readVal, err := changeAndGetProp(propName, writeVal)
+ if err != nil {
+ t.Error(err)
+ }
+
+ if readVal != writeVal {
+ t.Errorf("The value written, '%s', is not the same as the "+
+ "value read '%s'.", writeVal, readVal)
+ }
+}
+
+// TestWindowEvents creates a window, maps it, listens for configure notify
+// events, issues a configure request, and checks for the appropriate
+// configure notify event.
+// This probably violates the notion of "test one thing and test it well,"
+// but testing X stuff is unique since it involves so much state.
+// Each request is checked to make sure there are no errors returned. If there
+// is an error, the test is failed.
+// You may see a window appear quickly and then disappear. Do not be alarmed :P
+// It's possible that this test will yield a false negative because we cannot
+// control our environment. That is, the window manager could override the
+// placement set. However, we set override redirect on the window, so the
+// window manager *shouldn't* touch our window if it is well-behaved.
+func TestWindowEvents(t *testing.T) {
+ // The geometry to set the window.
+ gx, gy, gw, gh := 200, 400, 1000, 300
+
+ wid, err := X.NewId()
+ if err != nil {
+ t.Fatalf("NewId: %s", err)
+ }
+
+ screen := X.DefaultScreen() // alias
+ err = X.CreateWindowChecked(screen.RootDepth, wid, screen.Root,
+ 0, 0, 500, 500, 0,
+ WindowClassInputOutput, screen.RootVisual,
+ CwBackPixel|CwOverrideRedirect, []uint32{0xffffffff, 1}).Check()
+ if err != nil {
+ t.Fatalf("CreateWindow: %s", err)
+ }
+
+ err = X.MapWindowChecked(wid).Check()
+ if err != nil {
+ t.Fatalf("MapWindow: %s", err)
+ }
+
+ // We don't listen in the CreateWindow request so that we don't get
+ // a MapNotify event.
+ err = X.ChangeWindowAttributesChecked(wid,
+ CwEventMask, []uint32{EventMaskStructureNotify}).Check()
+ if err != nil {
+ t.Fatalf("ChangeWindowAttributes: %s", err)
+ }
+
+ err = X.ConfigureWindowChecked(wid,
+ ConfigWindowX|ConfigWindowY|
+ ConfigWindowWidth|ConfigWindowHeight,
+ []uint32{uint32(gx), uint32(gy), uint32(gw), uint32(gh)}).Check()
+ if err != nil {
+ t.Fatalf("ConfigureWindow: %s", err)
+ }
+
+ evOrErr := waitForEvent(t, 5)
+ switch event := evOrErr.ev.(type) {
+ case ConfigureNotifyEvent:
+ if event.X != int16(gx) {
+ t.Fatalf("x was set to %d but ConfigureNotify reports %d",
+ gx, event.X)
+ }
+ if event.Y != int16(gy) {
+ t.Fatalf("y was set to %d but ConfigureNotify reports %d",
+ gy, event.Y)
+ }
+ if event.Width != uint16(gw) {
+ t.Fatalf("width was set to %d but ConfigureNotify reports %d",
+ gw, event.Width)
+ }
+ if event.Height != uint16(gh) {
+ t.Fatalf("height was set to %d but ConfigureNotify reports %d",
+ gh, event.Height)
+ }
+ default:
+ t.Fatalf("Expected a ConfigureNotifyEvent but got %T instead.", event)
+ }
+
+ // Okay, clean up!
+ err = X.ChangeWindowAttributesChecked(wid,
+ CwEventMask, []uint32{0}).Check()
+ if err != nil {
+ t.Fatalf("ChangeWindowAttributes: %s", err)
+ }
+
+ err = X.DestroyWindowChecked(wid).Check()
+ if err != nil {
+ t.Fatalf("DestroyWindow: %s", err)
+ }
+}
+
+/******************************************************************************/
+// Benchmarks
+/******************************************************************************/
+
+// BenchmarkInternAtomsGood shows how many requests with replies
+// *should* be sent and gathered from the server. Namely, send as many
+// requests as you can at once, then go back and gather up all the replies.
+// More importantly, this approach can exploit parallelism better when
+// GOMAXPROCS > 1.
+// Run with `go test -run 'nomatch' -bench '.*' -cpu 1,2,6` if you have
+// multiple cores to see the improvement that parallelism brings.
+func BenchmarkInternAtomsGood(b *testing.B) {
+ b.StopTimer()
+ names := seqNames(b.N)
+
+ b.StartTimer()
+ cookies := make([]InternAtomCookie, b.N)
+ for i := 0; i < b.N; i++ {
+ cookies[i] = X.InternAtom(false, uint16(len(names[i])), names[i])
+ }
+ for _, cookie := range cookies {
+ cookie.Reply()
+ }
+}
+
+// BenchmarkInternAtomsBad shows how *not* to issue a lot of requests with
+// replies. Namely, each subsequent request isn't issued *until* the last
+// reply is made. This implies a round trip to the X server for every
+// iteration.
+func BenchmarkInternAtomsPoor(b *testing.B) {
+ b.StopTimer()
+ names := seqNames(b.N)
+
+ b.StartTimer()
+ for i := 0; i < b.N; i++ {
+ X.InternAtom(false, uint16(len(names[i])), names[i]).Reply()
+ }
+}
+
+/******************************************************************************/
+// Helper functions
+/******************************************************************************/
+
+// changeAndGetProp sets property 'prop' with value 'val'.
+// It then gets the value of that property and returns it.
+// (It's used to check that the 'val' going in is the same 'val' going out.)
+// It tests both requests with and without replies (GetProperty and
+// ChangeProperty respectively.)
+func changeAndGetProp(prop, val string) (string, error) {
+ propAtom, err := X.InternAtom(false, uint16(len(prop)), prop).Reply()
+ if err != nil {
+ return "", fmt.Errorf("InternAtom: %s", err)
+ }
+
+ typName := "UTF8_STRING"
+ typAtom, err := X.InternAtom(false, uint16(len(typName)), typName).Reply()
+ if err != nil {
+ return "", fmt.Errorf("InternAtom: %s", err)
+ }
+
+ err = X.ChangePropertyChecked(PropModeReplace, X.DefaultScreen().Root,
+ propAtom.Atom, typAtom.Atom, 8, uint32(len(val)), []byte(val)).Check()
+ if err != nil {
+ return "", fmt.Errorf("ChangeProperty: %s", err)
+ }
+
+ reply, err := X.GetProperty(false, X.DefaultScreen().Root, propAtom.Atom,
+ GetPropertyTypeAny, 0, (1<<32)-1).Reply()
+ if err != nil {
+ return "", fmt.Errorf("GetProperty: %s", err)
+ }
+ if reply.Format != 8 {
+ return "", fmt.Errorf("Property reply format is %d but it should be 8.",
+ reply.Format)
+ }
+
+ return string(reply.Value), nil
+}
+
+// verifyMapWindowError takes an error that is returned with an invalid
+// MapWindow request with a window Id of 0 and makes sure the error is the
+// right type and contains the correct values.
+func verifyMapWindowError(t *testing.T, err error) {
+ switch e := err.(type) {
+ case WindowError:
+ if e.BadValue != 0 {
+ t.Fatalf("WindowError should report a bad value of 0 but "+
+ "it reports %d instead.", e.BadValue)
+ }
+ if e.MajorOpcode != 8 {
+ t.Fatalf("WindowError should report a major opcode of 8 "+
+ "(which is a MapWindow request), but it reports %d instead.",
+ e.MajorOpcode)
+ }
+ default:
+ t.Fatalf("Expected a WindowError but got %T instead.", e)
+ }
+}
+
+// randString generates a random string of length n.
+func randString(n int) string {
+ byts := make([]byte, n)
+ for i := 0; i < n; i++ {
+ rando := rand.Intn(53)
+ switch {
+ case rando <= 25:
+ byts[i] = byte(65 + rando)
+ case rando <= 51:
+ byts[i] = byte(97 + rando - 26)
+ default:
+ byts[i] = ' '
+ }
+ }
+ return string(byts)
+}
+
+// seqNames creates a slice of NAME0, NAME1, ..., NAMEN.
+func seqNames(n int) []string {
+ names := make([]string, n)
+ for i := range names {
+ names[i] = fmt.Sprintf("NAME%d", i)
+ }
+ return names
+}
+
+// evErr represents a value that is either an event or an error.
+type evErr struct {
+ ev Event
+ err Error
+}
+
+// channel used to pass evErrs.
+var evOrErrChan = make(chan evErr, 0)
+
+// grabEvents is a goroutine that reads events off the wire.
+// We used this instead of WaitForEvent directly in our tests so that
+// we can timeout and fail a test.
+func grabEvents() {
+ for {
+ ev, err := X.WaitForEvent()
+ evOrErrChan <- evErr{ev, err}
+ }
+}
+
+// waitForEvent asks the evOrErrChan channel for an event.
+// If it doesn't get an event in 'n' seconds, the current test is failed.
+func waitForEvent(t *testing.T, n int) evErr {
+ var evOrErr evErr
+
+ select {
+ case evOrErr = <-evOrErrChan:
+ case <-time.After(time.Second * 5):
+ t.Fatalf("After waiting 5 seconds for an event or an error, " +
+ "we have timed out.")
+ }
+
+ return evOrErr
+}