| package xproto |
| |
| /* |
| 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.) |
| |
| In sum, these tests are more focused on testing the core xgb package itself, |
| rather than whether xproto has properly implemented the core X client |
| protocol. |
| */ |
| |
| import ( |
| "fmt" |
| "log" |
| "math/rand" |
| "os" |
| "testing" |
| "time" |
| |
| "golang.org/x/exp/shiny/internal/xgb" |
| ) |
| |
| // The X connection used throughout testing. |
| var X *xgb.Conn |
| |
| func TestMain(m *testing.M) { |
| if os.Getenv("DISPLAY") == "" { |
| log.Printf("No X; Skipping tests") |
| os.Exit(0) |
| } |
| |
| var err error |
| X, err = xgb.NewConn() |
| if err != nil { |
| log.Fatal(err) |
| } |
| rand.Seed(time.Now().UnixNano()) |
| go grabEvents() |
| |
| os.Exit(m.Run()) |
| } |
| |
| /******************************************************************************/ |
| // Tests |
| /******************************************************************************/ |
| |
| // TestSynchronousError purposefully causes a BadWindow error in a |
| // MapWindow request, and checks it synchronously. |
| func TestSynchronousError(t *testing.T) { |
| err := MapWindowChecked(X, 0).Check() // resource 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) { |
| MapWindow(X, 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++ { |
| NoOperation(X) |
| } |
| 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 := InternAtom(X, 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 := NewWindowId(X) |
| if err != nil { |
| t.Fatalf("NewId: %s", err) |
| } |
| |
| screen := Setup(X).DefaultScreen(X) // alias |
| err = CreateWindowChecked(X, 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 = MapWindowChecked(X, 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 = ChangeWindowAttributesChecked(X, wid, |
| CwEventMask, []uint32{EventMaskStructureNotify}).Check() |
| if err != nil { |
| t.Fatalf("ChangeWindowAttributes: %s", err) |
| } |
| |
| err = ConfigureWindowChecked(X, 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 = ChangeWindowAttributesChecked(X, wid, |
| CwEventMask, []uint32{0}).Check() |
| if err != nil { |
| t.Fatalf("ChangeWindowAttributes: %s", err) |
| } |
| |
| err = DestroyWindowChecked(X, wid).Check() |
| if err != nil { |
| t.Fatalf("DestroyWindow: %s", err) |
| } |
| } |
| |
| // Calls GetFontPath function, Issue #12 |
| func TestGetFontPath(t *testing.T) { |
| fontPathReply, err := GetFontPath(X).Reply() |
| if err != nil { |
| t.Fatalf("GetFontPath: %v", err) |
| } |
| _ = fontPathReply |
| } |
| |
| func TestListFonts(t *testing.T) { |
| listFontsReply, err := ListFonts(X, 10, 1, "*").Reply() |
| if err != nil { |
| t.Fatalf("ListFonts: %v", err) |
| } |
| _ = listFontsReply |
| } |
| |
| /******************************************************************************/ |
| // 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 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] = InternAtom(X, 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++ { |
| InternAtom(X, 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) { |
| setup := Setup(X) |
| root := setup.DefaultScreen(X).Root |
| |
| propAtom, err := InternAtom(X, false, uint16(len(prop)), prop).Reply() |
| if err != nil { |
| return "", fmt.Errorf("InternAtom: %s", err) |
| } |
| |
| typName := "UTF8_STRING" |
| typAtom, err := InternAtom(X, false, uint16(len(typName)), typName).Reply() |
| if err != nil { |
| return "", fmt.Errorf("InternAtom: %s", err) |
| } |
| |
| err = ChangePropertyChecked(X, PropModeReplace, root, propAtom.Atom, |
| typAtom.Atom, 8, uint32(len(val)), []byte(val)).Check() |
| if err != nil { |
| return "", fmt.Errorf("ChangeProperty: %s", err) |
| } |
| |
| reply, err := GetProperty(X, false, 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 xgb.Event |
| err xgb.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 |
| } |