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  }