package xturbo_test import ( "bytes" "encoding/binary" "testing" "bnema.dev/xturbo/internal/xtest" "bnema.dev/xturbo/render" "bnema.dev/xturbo/res" "bnema.dev/xturbo/xfixes" "bnema.dev/xturbo/xproto" ) func loadReply(t testing.TB, name string) []byte { t.Helper() raw, err := xtest.LoadHex("testdata/golden/" + name + ".reply.hex") if err != nil { t.Fatal(err) } if raw[0] != 1 || len(raw) != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) { t.Fatalf("%s: not a framed reply", name) } return raw } // Expected values are independent of the generated code: they follow from the // capture script (window geometry, written property, ConfigureWindow state) // or are read from the raw golden at protocol-spec offsets. func TestReplyGoldens(t *testing.T) { u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) } t.Run("xproto/GetGeometry", func(t *testing.T) { raw := loadReply(t, "xproto/GetGeometry") r, ok := xproto.ParseGetGeometryReply(raw) if !ok || r.Depth() != 24 || r.Root() != xproto.Window(u32(raw, 8)) || r.X() != 0 || r.Y() != 0 || r.Width() != 800 || r.Height() != 600 || r.BorderWidth() != 0 || !bytes.Equal(r.Raw(), raw) { t.Fatalf("decoded %+v", r) } }) t.Run("xproto/QueryTree", func(t *testing.T) { raw := loadReply(t, "xproto/QueryTree") r, ok := xproto.ParseQueryTreeReply(raw) if !ok || r.Root() != xproto.Window(u32(raw, 8)) || r.Parent() != xproto.WindowNone || r.ChildrenLen() != 2 { t.Fatal("fixed fields") } c := r.Children() // Stacking order bottom-to-top: the sibling was created before the child. if c.Len() != 2 || c.At(0)&0x1fffff != 2 || c.At(1)&0x1fffff != 1 || c.At(0)&^0x1fffff != c.At(1)&^0x1fffff { t.Fatalf("children %x %x", c.At(0), c.At(1)) } }) t.Run("xproto/GetWindowAttributes", func(t *testing.T) { raw := loadReply(t, "xproto/GetWindowAttributes") r, ok := xproto.ParseGetWindowAttributesReply(raw) if !ok || r.BackingStore() != xproto.BackingStoreNotUseful || r.Visual() != xproto.Visualid(u32(raw, 8)) || r.Class() != xproto.WindowClassInputOutput || r.BitGravity() != xproto.GravityBitForget || r.WinGravity() != xproto.GravityNorthWest || r.BackingPlanes() != 0xffffffff || r.BackingPixel() != 0 || !r.SaveUnder() || !r.MapIsInstalled() || r.MapState() != xproto.MapStateUnmapped || r.OverrideRedirect() || r.Colormap() != xproto.Colormap(u32(raw, 28)) || r.AllEventMasks() != 0 || r.YourEventMask() != 0 || r.DoNotPropagateMask() != 0 { t.Fatal("decoded attributes") } }) t.Run("xproto/InternAtom", func(t *testing.T) { raw := loadReply(t, "xproto/InternAtom") r, ok := xproto.ParseInternAtomReply(raw) if !ok || r.Atom() == xproto.AtomNone || r.Atom() != xproto.Atom(u32(raw, 8)) { t.Fatal("atom") } }) t.Run("xproto/GetProperty", func(t *testing.T) { raw := loadReply(t, "xproto/GetProperty") r, ok := xproto.ParseGetPropertyReply(raw) if !ok || r.Format() != 8 || r.Type() != xproto.AtomString || r.BytesAfter() != 0 || r.ValueLen() != 3 || string(r.Value()) != "abc" || cap(r.Value()) != 3 { t.Fatalf("value %q", r.Value()) } }) t.Run("xproto/GetPropertyEmpty", func(t *testing.T) { raw := loadReply(t, "xproto/GetPropertyEmpty") r, ok := xproto.ParseGetPropertyReply(raw) if !ok || r.Format() != 0 || r.Type() != xproto.AtomNone || r.ValueLen() != 0 || len(r.Value()) != 0 { t.Fatal("empty property") } }) t.Run("xproto/QueryExtension", func(t *testing.T) { raw := loadReply(t, "xproto/QueryExtension") r, ok := xproto.ParseQueryExtensionReply(raw) if !ok || !r.Present() || r.MajorOpcode() < 128 || r.FirstEvent() != raw[10] || r.FirstError() != raw[11] { t.Fatal("XFIXES query") } }) t.Run("xproto/ListExtensions", func(t *testing.T) { raw := loadReply(t, "xproto/ListExtensions") r, ok := xproto.ParseListExtensionsReply(raw) if !ok { t.Fatal("parse") } it := r.Names() var names []string total := 32 for it.Next() { v := it.Value() if int(v.NameLen()) != len(v.Name()) || v.Size() != 1+len(v.Name()) { t.Fatal("STR view") } names = append(names, string(v.Name())) total += v.Size() } if it.Err() || len(names) != int(r.NamesLen()) || total > len(raw) || len(raw)-total > 3 { t.Fatalf("iterated %d of %d, %d/%d bytes", len(names), r.NamesLen(), total, len(raw)) } found := map[string]bool{} for _, n := range names { found[n] = true } for _, want := range []string{"XFIXES", "RENDER", "BIG-REQUESTS", "Composite", "X-Resource"} { if !found[want] { t.Fatalf("missing %s in %q", want, names) } } }) t.Run("xfixes/QueryVersion", func(t *testing.T) { r, ok := xfixes.ParseQueryVersionReply(loadReply(t, "xfixes/QueryVersion")) if !ok || r.MajorVersion() != 6 || r.MinorVersion() != 0 { t.Fatal("version") } }) t.Run("res/QueryClientIds", func(t *testing.T) { r, ok := res.ParseQueryClientIdsReply(loadReply(t, "res/QueryClientIds")) if !ok || r.NumIds() != 2 { t.Fatal("parse") } // Spec client 0 asks for every client's XID: the server client and ours. it := r.Ids() var clients []uint32 for it.Next() { v := it.Value() if v.Spec().Mask != res.ClientIdMaskClientXID || v.Length() != 0 || v.Value().Len() != 0 || v.Size() != 12 { t.Fatal("ClientIdValue view") } clients = append(clients, v.Spec().Client) } if it.Err() || len(clients) != 2 || clients[0] != 0 || clients[1] == 0 { t.Fatalf("clients %x", clients) } }) t.Run("render/QueryPictFormats", func(t *testing.T) { raw := loadReply(t, "render/QueryPictFormats") r, ok := render.ParseQueryPictFormatsReply(raw) if !ok || r.NumScreens() != 1 || r.Formats().Len() != int(r.NumFormats()) || r.Subpixels().Len() != int(r.NumSubpixel()) { t.Fatal("counts") } formats := map[render.Pictformat]bool{} for i := range r.Formats().Len() { f := r.Formats().At(i) formats[f.Id] = true } screens := r.Screens() visuals := 0 for screens.Next() { depths := screens.Value().Depths() for depths.Next() { vs := depths.Value().Visuals() for i := range vs.Len() { if !formats[vs.At(i).Format] { t.Fatal("visual references an unknown format") } visuals++ } } if depths.Err() { t.Fatal("depth iterator") } } if screens.Err() || visuals == 0 { t.Fatal("screen iterator") } for i := range r.Subpixels().Len() { if r.Subpixels().At(i) != render.SubPixelUnknown { t.Fatal("Xvfb subpixel order") } } }) } func TestReplyParseRejects(t *testing.T) { raw := loadReply(t, "xproto/QueryTree") if _, ok := xproto.ParseQueryTreeReply(raw[:31]); ok { t.Fatal("short fixed prefix accepted") } if _, ok := xproto.ParseQueryTreeReply(raw[:len(raw)-1]); ok { t.Fatal("truncated list accepted") } raw = loadReply(t, "xproto/GetWindowAttributes") if _, ok := xproto.ParseGetWindowAttributesReply(raw[:43]); ok { t.Fatal("44-byte reply prefix not enforced") } raw = bytes.Clone(loadReply(t, "xproto/ListExtensions")) raw[1] = 255 // names_len: far more STRs than the reply holds if _, ok := xproto.ParseListExtensionsReply(raw); ok { t.Fatal("STR count beyond the reply accepted") } prop := bytes.Clone(loadReply(t, "xproto/GetProperty")) prop[1] = 32 // 3 items of 4 bytes overrun the 36-byte reply if _, ok := xproto.ParseGetPropertyReply(prop); ok { t.Fatal("format*value_len overrun accepted") } } func TestReplyAllocs(t *testing.T) { prop := loadReply(t, "xproto/GetProperty") tree := loadReply(t, "xproto/QueryTree") exts := loadReply(t, "xproto/ListExtensions") formats := loadReply(t, "render/QueryPictFormats") ids := loadReply(t, "res/QueryClientIds") var sink uint64 n := testing.AllocsPerRun(1000, func() { p, _ := xproto.ParseGetPropertyReply(prop) v := p.Value() sink += uint64(len(v)) + uint64(p.Type()) + uint64(p.Format()) q, _ := xproto.ParseQueryTreeReply(tree) c := q.Children() for i := range c.Len() { sink += uint64(c.At(i)) } sink += uint64(q.Root()) + uint64(q.Parent()) l, _ := xproto.ParseListExtensionsReply(exts) it := l.Names() for it.Next() { sink += uint64(len(it.Value().Name())) } f, _ := render.ParseQueryPictFormatsReply(formats) fl := f.Formats() for i := range fl.Len() { sink += uint64(fl.At(i).Id) } s := f.Screens() for s.Next() { d := s.Value().Depths() for d.Next() { vs := d.Value().Visuals() for i := range vs.Len() { sink += uint64(vs.At(i).Visual) } } } sp := f.Subpixels() for i := range sp.Len() { sink += uint64(sp.At(i)) } r, _ := res.ParseQueryClientIdsReply(ids) ri := r.Ids() for ri.Next() { sink += uint64(ri.Value().Value().Len()) } }) if n != 0 { t.Fatalf("reply parse/getters/iteration allocate %v times", n) } _ = sink } func BenchmarkGetPropertyReply(b *testing.B) { raw := loadReply(b, "xproto/GetProperty") var sink int b.ReportAllocs() for b.Loop() { r, ok := xproto.ParseGetPropertyReply(raw) if !ok { b.Fatal("parse") } sink += len(r.Value()) + int(r.Type()) } _ = sink } func BenchmarkQueryTreeReply(b *testing.B) { raw := loadReply(b, "xproto/QueryTree") var sink xproto.Window b.ReportAllocs() for b.Loop() { r, _ := xproto.ParseQueryTreeReply(raw) c := r.Children() for i := range c.Len() { sink ^= c.At(i) } } _ = sink } func BenchmarkListExtensionsReply(b *testing.B) { raw := loadReply(b, "xproto/ListExtensions") var sink int b.ReportAllocs() for b.Loop() { r, _ := xproto.ParseListExtensionsReply(raw) it := r.Names() for it.Next() { sink += len(it.Value().Name()) } } _ = sink }