test(xtest): compare live Xvfb captures independently of the server build
The live comparison failed on other Xvfb builds because goldens embed server-chosen values. Each golden now lists its server-chosen fields by offset (root window, visual, colormap, interned atom, resource IDs, extension opcodes and event/error bases, timestamps). -update records their capture-time values in testdata/golden/xvfb-roles.json; compare mode checks the golden holds the recorded values and the live packet the live server's, masks both and compares the rest exactly. ListExtensions and Render QueryPictFormats depend on the server build, so live compare checks their framing, list counts, bound extensions and the root visual's picture format instead of bytes. XTURBO_XVFB_ARGS adds Xvfb options to simulate another server layout. Committed goldens and unit tests are unchanged.
3 files · +296 -36
M internal/xtest/README.md +21 -4
| @@ -1,10 +1,27 @@ | |||
| 1 | 1 | # Wire goldens | |
| 2 | 2 | ||
| 3 | 3 | `make xserver` captures from a fresh Xvfb and compares every packet with the | |
| 4 | - | committed golden, failing on any difference. Server timestamps | |
| 5 | - | (PropertyNotify time, XFIXES SelectionNotify timestamps) vary per run and are | |
| 6 | - | excluded from the comparison. The test is skipped when Xvfb is not installed. | |
| 7 | - | Rewrite the goldens from the repository root with: | |
| 4 | + | committed golden. Comparison is independent of the server build: | |
| 5 | + | ||
| 6 | + | - Server-chosen values (root window, visual, colormap, interned atom, | |
| 7 | + | resource IDs from the client's resource base, extension opcodes and | |
| 8 | + | event/error bases) are listed per golden by field offset in | |
| 9 | + | `serverFields`. `-update` records their capture-time values in | |
| 10 | + | `testdata/golden/xvfb-roles.json`. Compare mode checks that the golden holds | |
| 11 | + | the recorded value and the live packet holds the live server's value, masks | |
| 12 | + | both, then compares the remaining bytes exactly. Server timestamps are | |
| 13 | + | masked without a value check. | |
| 14 | + | - `ListExtensions` and Render `QueryPictFormats` replies depend on how the | |
| 15 | + | server was built (extension list, visuals, formats), so live compare | |
| 16 | + | checks invariants: framing, counts matching list lengths, the bound | |
| 17 | + | extensions present, the root visual mapped to the format CreatePicture uses. | |
| 18 | + | ||
| 19 | + | Unit tests still decode the committed goldens byte-exactly. The test is | |
| 20 | + | skipped when Xvfb is not installed. `XTURBO_XVFB_ARGS` adds Xvfb options to | |
| 21 | + | simulate another server layout, e.g. `XTURBO_XVFB_ARGS="-extension GLX"`. | |
| 22 | + | Screen size and depth (800x600x24) and the bound extensions are capture | |
| 23 | + | inputs and must stay as captured. Rewrite the goldens from the repository | |
| 24 | + | root with: | |
| 8 | 25 | ||
| 9 | 26 | ``` | |
| 10 | 27 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | |
| @@ -1,10 +1,27 @@ | |||||
| 1 | # Wire goldens | 1 | # Wire goldens | ||
| 2 | 2 | ||||
| 3 | `make xserver` captures from a fresh Xvfb and compares every packet with the | 3 | `make xserver` captures from a fresh Xvfb and compares every packet with the | ||
| 4 | - | committed golden, failing on any difference. Server timestamps | 4 | + | committed golden. Comparison is independent of the server build: |
| 5 | - | (PropertyNotify time, XFIXES SelectionNotify timestamps) vary per run and are | 5 | + | |
| 6 | - | excluded from the comparison. The test is skipped when Xvfb is not installed. | 6 | + | - Server-chosen values (root window, visual, colormap, interned atom, |
| 7 | - | Rewrite the goldens from the repository root with: | 7 | + | resource IDs from the client's resource base, extension opcodes and |
| 8 | + | event/error bases) are listed per golden by field offset in | |||
| 9 | + | `serverFields`. `-update` records their capture-time values in | |||
| 10 | + | `testdata/golden/xvfb-roles.json`. Compare mode checks that the golden holds | |||
| 11 | + | the recorded value and the live packet holds the live server's value, masks | |||
| 12 | + | both, then compares the remaining bytes exactly. Server timestamps are | |||
| 13 | + | masked without a value check. | |||
| 14 | + | - `ListExtensions` and Render `QueryPictFormats` replies depend on how the | |||
| 15 | + | server was built (extension list, visuals, formats), so live compare | |||
| 16 | + | checks invariants: framing, counts matching list lengths, the bound | |||
| 17 | + | extensions present, the root visual mapped to the format CreatePicture uses. | |||
| 18 | + | ||||
| 19 | + | Unit tests still decode the committed goldens byte-exactly. The test is | |||
| 20 | + | skipped when Xvfb is not installed. `XTURBO_XVFB_ARGS` adds Xvfb options to | |||
| 21 | + | simulate another server layout, e.g. `XTURBO_XVFB_ARGS="-extension GLX"`. | |||
| 22 | + | Screen size and depth (800x600x24) and the bound extensions are capture | |||
| 23 | + | inputs and must stay as captured. Rewrite the goldens from the repository | |||
| 24 | + | root with: | |||
| 8 | 25 | ||||
| 9 | ``` | 26 | ``` | ||
| 10 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | 27 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | ||
M internal/xtest/capture_test.go +256 -32
| @@ -6,13 +6,16 @@ import ( | |||
| 6 | 6 | "bytes" | |
| 7 | 7 | "encoding/binary" | |
| 8 | 8 | "encoding/hex" | |
| 9 | + | "encoding/json" | |
| 9 | 10 | "flag" | |
| 10 | 11 | "fmt" | |
| 11 | 12 | "io" | |
| 13 | + | "maps" | |
| 12 | 14 | "net" | |
| 13 | 15 | "os" | |
| 14 | 16 | "os/exec" | |
| 15 | 17 | "path/filepath" | |
| 18 | + | "slices" | |
| 16 | 19 | "strconv" | |
| 17 | 20 | "strings" | |
| 18 | 21 | "testing" | |
| @@ -23,11 +26,67 @@ import ( | |||
| 23 | 26 | ||
| 24 | 27 | var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them") | |
| 25 | 28 | ||
| 26 | - | // volatile lists server-time fields that differ on every run; comparison | |
| 27 | - | // ignores these byte ranges [from, to). Everything else must match exactly. | |
| 28 | - | var volatile = map[string][][2]int{ | |
| 29 | - | "xproto/PropertyNotify.event.hex": {{12, 16}}, // time | |
| 30 | - | "xfixes/SelectionNotify.event.hex": {{16, 24}}, // timestamp, selection_timestamp | |
| 29 | + | // field is a server-dependent value inside a golden: the size-byte | |
| 30 | + | // little-endian integer at Off must equal the server's value for Role. | |
| 31 | + | type field struct { | |
| 32 | + | Off, Size int | |
| 33 | + | Role string | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // serverFields lists, per golden, every value the X server chooses: resource | |
| 37 | + | // IDs derived from a server-assigned base, the root window and its visual and | |
| 38 | + | // colormap, interned atoms, extension opcodes and bases, and timestamps. | |
| 39 | + | // Compare mode checks that the golden holds the value recorded at capture | |
| 40 | + | // (xvfb-roles.json) and the live packet holds the live server's value, then | |
| 41 | + | // masks both and compares the remaining bytes exactly. Role "time" is a | |
| 42 | + | // per-run server timestamp and is masked without a value check. | |
| 43 | + | var serverFields = map[string][]field{ | |
| 44 | + | "xproto/MapWindow.req": {{4, 4, "root"}}, | |
| 45 | + | "xproto/GetGeometry.req": {{4, 4, "root"}}, | |
| 46 | + | "xproto/ChangeProperty.req": {{4, 4, "root"}, {8, 4, "atom"}}, | |
| 47 | + | "xproto/CreateWindow.req": {{4, 4, "child"}, {8, 4, "root"}}, | |
| 48 | + | "xproto/ChangeWindowAttributes.req": {{4, 4, "child"}}, | |
| 49 | + | "xproto/ConfigureWindow.req": {{4, 4, "child"}}, | |
| 50 | + | "xproto/ConfigureWindowSparse.req": {{4, 4, "child"}}, | |
| 51 | + | "xproto/ConfigureWindowAll.req": {{4, 4, "child"}, {32, 4, "sibling"}}, | |
| 52 | + | "render/CreatePicture.req": {{0, 1, "renderMajor"}, {4, 4, "picture"}, {8, 4, "child"}, {12, 4, "rootFormat"}}, | |
| 53 | + | "res/QueryClientIds.req": {{0, 1, "resMajor"}}, | |
| 54 | + | "xfixes/QueryVersion.req": {{0, 1, "xfixesMajor"}}, | |
| 55 | + | "xproto/QueryExtension.reply": {{9, 1, "xfixesMajor"}, {10, 1, "xfixesEvent"}, {11, 1, "xfixesError"}}, | |
| 56 | + | "xproto/GetGeometry.reply": {{8, 4, "root"}}, | |
| 57 | + | "xproto/InternAtom.reply": {{8, 4, "atom"}}, | |
| 58 | + | "xproto/GetWindowAttributes.reply": {{8, 4, "rootVisual"}, {28, 4, "rootColormap"}}, | |
| 59 | + | "xproto/QueryTree.reply": {{8, 4, "root"}, {32, 4, "sibling"}, {36, 4, "child"}}, | |
| 60 | + | "res/QueryClientIds.reply": {{44, 4, "base"}}, | |
| 61 | + | "xproto/CreateNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 62 | + | "xproto/MapRequest.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 63 | + | "xproto/ConfigureRequest.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 64 | + | "xproto/MapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 65 | + | "xproto/UnmapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 66 | + | "xproto/DestroyNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |
| 67 | + | "xproto/PropertyNotify.event": {{4, 4, "root"}, {8, 4, "atom"}, {12, 4, "time"}}, | |
| 68 | + | "xproto/ClientMessage.event": {{4, 4, "client2"}, {8, 4, "atom"}}, | |
| 69 | + | "xproto/SelectionNotify.event": {{8, 4, "child"}}, | |
| 70 | + | "xfixes/SelectionNotify.event": {{0, 1, "xfixesEvent"}, {4, 4, "root"}, {8, 4, "client2"}, {16, 4, "time"}, {20, 4, "time"}}, | |
| 71 | + | "xproto/BadWindow.error": {{4, 4, "badWindow"}}, | |
| 72 | + | "xfixes/BadRegion.error": {{1, 1, "xfixesError"}, {4, 4, "badRegion"}, {10, 1, "xfixesMajor"}}, | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | // structural replies depend on the server build itself (extension list, | |
| 76 | + | // visuals and picture formats), so live compare checks invariants instead of | |
| 77 | + | // bytes; unit tests still decode the committed golden byte-exactly. | |
| 78 | + | var structural = map[string]func(p []byte, roles map[string]uint32) error{ | |
| 79 | + | "xproto/ListExtensions.reply": checkListExtensions, | |
| 80 | + | "render/QueryPictFormats.reply": checkQueryPictFormats, | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | const rolesPath = "../../testdata/golden/xvfb-roles.json" | |
| 84 | + | ||
| 85 | + | // xvfbArgs lets a run simulate a different server layout, e.g. | |
| 86 | + | // XTURBO_XVFB_ARGS="-extension GLX" shifts resource IDs, atoms and visuals. | |
| 87 | + | func xvfbArgs() []string { | |
| 88 | + | args := []string{"-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac"} | |
| 89 | + | return append(args, strings.Fields(os.Getenv("XTURBO_XVFB_ARGS"))...) | |
| 31 | 90 | } | |
| 32 | 91 | ||
| 33 | 92 | // TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By | |
| @@ -43,7 +102,7 @@ func TestCaptureRequests(t *testing.T) { | |||
| 43 | 102 | t.Fatal(err) | |
| 44 | 103 | } | |
| 45 | 104 | defer r.Close() | |
| 46 | - | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac") | |
| 105 | + | cmd := exec.Command("Xvfb", xvfbArgs()...) | |
| 47 | 106 | cmd.ExtraFiles = []*os.File{w} | |
| 48 | 107 | var log bytes.Buffer | |
| 49 | 108 | cmd.Stderr = &log | |
| @@ -512,12 +571,6 @@ func TestCaptureRequests(t *testing.T) { | |||
| 512 | 571 | if len(events) != 10 || len(xerrors) != 3 { | |
| 513 | 572 | t.Fatalf("captured %d events, %d errors", len(events), len(xerrors)) | |
| 514 | 573 | } | |
| 515 | - | for name, p := range events { | |
| 516 | - | golden(t, name+".event.hex", "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n", p) | |
| 517 | - | } | |
| 518 | - | for name, p := range xerrors { | |
| 519 | - | golden(t, name+".error.hex", "# source: Xvfb error via raw socketpair tee to an independent reference request\n", p) | |
| 520 | - | } | |
| 521 | 574 | // Closing the client drains the request direction; wait before inspecting tee. | |
| 522 | 575 | client.Close() | |
| 523 | 576 | select { | |
| @@ -577,40 +630,211 @@ func TestCaptureRequests(t *testing.T) { | |||
| 577 | 630 | if len(captured) != 12 || len(replies) != 11 { | |
| 578 | 631 | t.Fatal("missing captured requests or replies") | |
| 579 | 632 | } | |
| 633 | + | out := map[string][]byte{} | |
| 580 | 634 | for name, p := range replies { | |
| 581 | - | golden(t, name+".reply.hex", "# source: Xvfb reply via raw socketpair tee to an independent reference request\n", p) | |
| 635 | + | out[name+".reply"] = p | |
| 582 | 636 | } | |
| 583 | 637 | for name, p := range captured { | |
| 584 | - | golden(t, name+".req.hex", "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n", p) | |
| 638 | + | out[name+".req"] = p | |
| 585 | 639 | } | |
| 586 | - | } | |
| 587 | - | ||
| 588 | - | // golden writes p (with -update) or compares it with the committed golden. | |
| 589 | - | func golden(t *testing.T, name, source string, p []byte) { | |
| 590 | - | t.Helper() | |
| 591 | - | path := filepath.Join("../../testdata/golden", name) | |
| 592 | - | if !*update { | |
| 593 | - | want, err := LoadHex(path) | |
| 594 | - | if err != nil { | |
| 595 | - | t.Errorf("%s: %v (run with -update to create it)", name, err) | |
| 596 | - | return | |
| 640 | + | for name, p := range events { | |
| 641 | + | out[name+".event"] = p | |
| 642 | + | } | |
| 643 | + | for name, p := range xerrors { | |
| 644 | + | out[name+".error"] = p | |
| 645 | + | } | |
| 646 | + | // Server-chosen values, keyed by the roles used in serverFields. | |
| 647 | + | roles := map[string]uint32{ | |
| 648 | + | "root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture, | |
| 649 | + | "client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888, | |
| 650 | + | "rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format, | |
| 651 | + | "xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError), | |
| 652 | + | "resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor), | |
| 653 | + | } | |
| 654 | + | if *update { | |
| 655 | + | for name, p := range out { | |
| 656 | + | writeGolden(t, name, p) | |
| 597 | 657 | } | |
| 598 | - | got := bytes.Clone(p) | |
| 599 | - | if len(got) == len(want) { | |
| 600 | - | for _, r := range volatile[name] { | |
| 601 | - | copy(got[r[0]:r[1]], want[r[0]:r[1]]) | |
| 602 | - | } | |
| 658 | + | rolesJSON, err := json.MarshalIndent(roles, "", " ") | |
| 659 | + | if err != nil { | |
| 660 | + | t.Fatal(err) | |
| 603 | 661 | } | |
| 604 | - | if !bytes.Equal(got, want) { | |
| 605 | - | t.Errorf("%s differs from live Xvfb:\n got %x\nwant %x", name, p, want) | |
| 662 | + | if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil { | |
| 663 | + | t.Fatal(err) | |
| 606 | 664 | } | |
| 607 | 665 | return | |
| 608 | 666 | } | |
| 667 | + | rolesJSON, err := os.ReadFile(rolesPath) | |
| 668 | + | if err != nil { | |
| 669 | + | t.Fatal(err) | |
| 670 | + | } | |
| 671 | + | var capturedRoles map[string]uint32 | |
| 672 | + | if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil { | |
| 673 | + | t.Fatal(err) | |
| 674 | + | } | |
| 675 | + | for _, role := range slices.Sorted(maps.Keys(roles)) { | |
| 676 | + | if roles[role] != capturedRoles[role] { | |
| 677 | + | t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role]) | |
| 678 | + | } | |
| 679 | + | } | |
| 680 | + | for name, p := range out { | |
| 681 | + | compareGolden(t, name, p, roles, capturedRoles) | |
| 682 | + | } | |
| 683 | + | } | |
| 684 | + | ||
| 685 | + | var sources = map[string]string{ | |
| 686 | + | "req": "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n", | |
| 687 | + | "reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n", | |
| 688 | + | "event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n", | |
| 689 | + | "error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n", | |
| 690 | + | } | |
| 691 | + | ||
| 692 | + | func writeGolden(t *testing.T, name string, p []byte) { | |
| 693 | + | t.Helper() | |
| 694 | + | path := filepath.Join("../../testdata/golden", name+".hex") | |
| 609 | 695 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |
| 610 | 696 | t.Fatal(err) | |
| 611 | 697 | } | |
| 698 | + | source := sources[name[strings.LastIndexByte(name, '.')+1:]] | |
| 612 | 699 | text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | |
| 613 | 700 | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |
| 614 | 701 | t.Fatal(err) | |
| 615 | 702 | } | |
| 616 | 703 | } | |
| 704 | + | ||
| 705 | + | // compareGolden checks a live packet against the committed golden without | |
| 706 | + | // depending on the server build: server-chosen fields must hold each side's | |
| 707 | + | // own value (live roles, recorded roles) and are masked before an exact | |
| 708 | + | // comparison; structural replies are checked for invariants only. | |
| 709 | + | func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) { | |
| 710 | + | t.Helper() | |
| 711 | + | want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex")) | |
| 712 | + | if err != nil { | |
| 713 | + | t.Errorf("%s: %v (run with -update to create it)", name, err) | |
| 714 | + | return | |
| 715 | + | } | |
| 716 | + | if check := structural[name]; check != nil { | |
| 717 | + | if err := check(live, roles); err != nil { | |
| 718 | + | t.Errorf("%s (live): %v", name, err) | |
| 719 | + | } | |
| 720 | + | if err := check(want, recorded); err != nil { | |
| 721 | + | t.Errorf("%s (golden): %v", name, err) | |
| 722 | + | } | |
| 723 | + | return | |
| 724 | + | } | |
| 725 | + | got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil) | |
| 726 | + | for _, e := range []struct { | |
| 727 | + | side string | |
| 728 | + | err error | |
| 729 | + | }{{"live", got.err}, {"golden", exp.err}} { | |
| 730 | + | if e.err != nil { | |
| 731 | + | t.Errorf("%s (%s): %v", name, e.side, e.err) | |
| 732 | + | err = e.err | |
| 733 | + | } | |
| 734 | + | } | |
| 735 | + | if err == nil && !bytes.Equal(got.b, exp.b) { | |
| 736 | + | t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want) | |
| 737 | + | } | |
| 738 | + | } | |
| 739 | + | ||
| 740 | + | type normalized struct { | |
| 741 | + | b []byte | |
| 742 | + | err error | |
| 743 | + | } | |
| 744 | + | ||
| 745 | + | // normalize verifies each server-chosen field of p holds roles[Role] and | |
| 746 | + | // replaces it with zero bytes. | |
| 747 | + | func normalize(name string, p []byte, roles map[string]uint32) normalized { | |
| 748 | + | b := bytes.Clone(p) | |
| 749 | + | for _, f := range serverFields[name] { | |
| 750 | + | if f.Off+f.Size > len(b) { | |
| 751 | + | return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))} | |
| 752 | + | } | |
| 753 | + | if f.Role != "time" { | |
| 754 | + | v, ok := roles[f.Role] | |
| 755 | + | if !ok { | |
| 756 | + | return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)} | |
| 757 | + | } | |
| 758 | + | var got uint32 | |
| 759 | + | if f.Size == 1 { | |
| 760 | + | got = uint32(b[f.Off]) | |
| 761 | + | } else { | |
| 762 | + | got = binary.LittleEndian.Uint32(b[f.Off:]) | |
| 763 | + | } | |
| 764 | + | if got != v { | |
| 765 | + | return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)} | |
| 766 | + | } | |
| 767 | + | } | |
| 768 | + | clear(b[f.Off : f.Off+f.Size]) | |
| 769 | + | } | |
| 770 | + | return normalized{b: b} | |
| 771 | + | } | |
| 772 | + | ||
| 773 | + | // checkListExtensions: a framed reply whose STR list holds exactly names_len | |
| 774 | + | // entries filling the reply, including the extensions xturbo binds. | |
| 775 | + | func checkListExtensions(p []byte, _ map[string]uint32) error { | |
| 776 | + | if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) { | |
| 777 | + | return fmt.Errorf("bad framing") | |
| 778 | + | } | |
| 779 | + | names := map[string]bool{} | |
| 780 | + | off := 32 | |
| 781 | + | for range int(p[1]) { | |
| 782 | + | if off >= len(p) || off+1+int(p[off]) > len(p) { | |
| 783 | + | return fmt.Errorf("STR list overruns the reply") | |
| 784 | + | } | |
| 785 | + | names[string(p[off+1:off+1+int(p[off])])] = true | |
| 786 | + | off += 1 + int(p[off]) | |
| 787 | + | } | |
| 788 | + | if (off+3)&^3 != len(p) { | |
| 789 | + | return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p)) | |
| 790 | + | } | |
| 791 | + | for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} { | |
| 792 | + | if !names[want] { | |
| 793 | + | return fmt.Errorf("missing extension %q", want) | |
| 794 | + | } | |
| 795 | + | } | |
| 796 | + | return nil | |
| 797 | + | } | |
| 798 | + | ||
| 799 | + | // checkQueryPictFormats: declared counts match the list sizes exactly and the | |
| 800 | + | // root visual maps to the picture format CreatePicture used. | |
| 801 | + | func checkQueryPictFormats(p []byte, roles map[string]uint32) error { | |
| 802 | + | u32 := binary.LittleEndian.Uint32 | |
| 803 | + | if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) { | |
| 804 | + | return fmt.Errorf("bad framing") | |
| 805 | + | } | |
| 806 | + | formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:])) | |
| 807 | + | off := 32 + 28*formats | |
| 808 | + | found := false | |
| 809 | + | for range screens { | |
| 810 | + | if off+8 > len(p) { | |
| 811 | + | return fmt.Errorf("screens overrun") | |
| 812 | + | } | |
| 813 | + | depths := int(u32(p[off:])) | |
| 814 | + | off += 8 | |
| 815 | + | for range depths { | |
| 816 | + | if off+8 > len(p) { | |
| 817 | + | return fmt.Errorf("depths overrun") | |
| 818 | + | } | |
| 819 | + | visuals := int(binary.LittleEndian.Uint16(p[off+2:])) | |
| 820 | + | off += 8 | |
| 821 | + | for range visuals { | |
| 822 | + | if off+8 > len(p) { | |
| 823 | + | return fmt.Errorf("visuals overrun") | |
| 824 | + | } | |
| 825 | + | if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] { | |
| 826 | + | found = true | |
| 827 | + | } | |
| 828 | + | off += 8 | |
| 829 | + | } | |
| 830 | + | } | |
| 831 | + | } | |
| 832 | + | off += 4 * subpixels | |
| 833 | + | if off != len(p) { | |
| 834 | + | return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p)) | |
| 835 | + | } | |
| 836 | + | if !found { | |
| 837 | + | return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"]) | |
| 838 | + | } | |
| 839 | + | return nil | |
| 840 | + | } | |
| @@ -6,13 +6,16 @@ import ( | |||||
| 6 | "bytes" | 6 | "bytes" | ||
| 7 | "encoding/binary" | 7 | "encoding/binary" | ||
| 8 | "encoding/hex" | 8 | "encoding/hex" | ||
| 9 | + | "encoding/json" | |||
| 9 | "flag" | 10 | "flag" | ||
| 10 | "fmt" | 11 | "fmt" | ||
| 11 | "io" | 12 | "io" | ||
| 13 | + | "maps" | |||
| 12 | "net" | 14 | "net" | ||
| 13 | "os" | 15 | "os" | ||
| 14 | "os/exec" | 16 | "os/exec" | ||
| 15 | "path/filepath" | 17 | "path/filepath" | ||
| 18 | + | "slices" | |||
| 16 | "strconv" | 19 | "strconv" | ||
| 17 | "strings" | 20 | "strings" | ||
| 18 | "testing" | 21 | "testing" | ||
| @@ -23,11 +26,67 @@ import ( | |||||
| 23 | 26 | ||||
| 24 | var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them") | 27 | var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them") | ||
| 25 | 28 | ||||
| 26 | - | // volatile lists server-time fields that differ on every run; comparison | 29 | + | // field is a server-dependent value inside a golden: the size-byte |
| 27 | - | // ignores these byte ranges [from, to). Everything else must match exactly. | 30 | + | // little-endian integer at Off must equal the server's value for Role. |
| 28 | - | var volatile = map[string][][2]int{ | 31 | + | type field struct { |
| 29 | - | "xproto/PropertyNotify.event.hex": {{12, 16}}, // time | 32 | + | Off, Size int |
| 30 | - | "xfixes/SelectionNotify.event.hex": {{16, 24}}, // timestamp, selection_timestamp | 33 | + | Role string |
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // serverFields lists, per golden, every value the X server chooses: resource | |||
| 37 | + | // IDs derived from a server-assigned base, the root window and its visual and | |||
| 38 | + | // colormap, interned atoms, extension opcodes and bases, and timestamps. | |||
| 39 | + | // Compare mode checks that the golden holds the value recorded at capture | |||
| 40 | + | // (xvfb-roles.json) and the live packet holds the live server's value, then | |||
| 41 | + | // masks both and compares the remaining bytes exactly. Role "time" is a | |||
| 42 | + | // per-run server timestamp and is masked without a value check. | |||
| 43 | + | var serverFields = map[string][]field{ | |||
| 44 | + | "xproto/MapWindow.req": {{4, 4, "root"}}, | |||
| 45 | + | "xproto/GetGeometry.req": {{4, 4, "root"}}, | |||
| 46 | + | "xproto/ChangeProperty.req": {{4, 4, "root"}, {8, 4, "atom"}}, | |||
| 47 | + | "xproto/CreateWindow.req": {{4, 4, "child"}, {8, 4, "root"}}, | |||
| 48 | + | "xproto/ChangeWindowAttributes.req": {{4, 4, "child"}}, | |||
| 49 | + | "xproto/ConfigureWindow.req": {{4, 4, "child"}}, | |||
| 50 | + | "xproto/ConfigureWindowSparse.req": {{4, 4, "child"}}, | |||
| 51 | + | "xproto/ConfigureWindowAll.req": {{4, 4, "child"}, {32, 4, "sibling"}}, | |||
| 52 | + | "render/CreatePicture.req": {{0, 1, "renderMajor"}, {4, 4, "picture"}, {8, 4, "child"}, {12, 4, "rootFormat"}}, | |||
| 53 | + | "res/QueryClientIds.req": {{0, 1, "resMajor"}}, | |||
| 54 | + | "xfixes/QueryVersion.req": {{0, 1, "xfixesMajor"}}, | |||
| 55 | + | "xproto/QueryExtension.reply": {{9, 1, "xfixesMajor"}, {10, 1, "xfixesEvent"}, {11, 1, "xfixesError"}}, | |||
| 56 | + | "xproto/GetGeometry.reply": {{8, 4, "root"}}, | |||
| 57 | + | "xproto/InternAtom.reply": {{8, 4, "atom"}}, | |||
| 58 | + | "xproto/GetWindowAttributes.reply": {{8, 4, "rootVisual"}, {28, 4, "rootColormap"}}, | |||
| 59 | + | "xproto/QueryTree.reply": {{8, 4, "root"}, {32, 4, "sibling"}, {36, 4, "child"}}, | |||
| 60 | + | "res/QueryClientIds.reply": {{44, 4, "base"}}, | |||
| 61 | + | "xproto/CreateNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 62 | + | "xproto/MapRequest.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 63 | + | "xproto/ConfigureRequest.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 64 | + | "xproto/MapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 65 | + | "xproto/UnmapNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 66 | + | "xproto/DestroyNotify.event": {{4, 4, "root"}, {8, 4, "client2"}}, | |||
| 67 | + | "xproto/PropertyNotify.event": {{4, 4, "root"}, {8, 4, "atom"}, {12, 4, "time"}}, | |||
| 68 | + | "xproto/ClientMessage.event": {{4, 4, "client2"}, {8, 4, "atom"}}, | |||
| 69 | + | "xproto/SelectionNotify.event": {{8, 4, "child"}}, | |||
| 70 | + | "xfixes/SelectionNotify.event": {{0, 1, "xfixesEvent"}, {4, 4, "root"}, {8, 4, "client2"}, {16, 4, "time"}, {20, 4, "time"}}, | |||
| 71 | + | "xproto/BadWindow.error": {{4, 4, "badWindow"}}, | |||
| 72 | + | "xfixes/BadRegion.error": {{1, 1, "xfixesError"}, {4, 4, "badRegion"}, {10, 1, "xfixesMajor"}}, | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | // structural replies depend on the server build itself (extension list, | |||
| 76 | + | // visuals and picture formats), so live compare checks invariants instead of | |||
| 77 | + | // bytes; unit tests still decode the committed golden byte-exactly. | |||
| 78 | + | var structural = map[string]func(p []byte, roles map[string]uint32) error{ | |||
| 79 | + | "xproto/ListExtensions.reply": checkListExtensions, | |||
| 80 | + | "render/QueryPictFormats.reply": checkQueryPictFormats, | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | const rolesPath = "../../testdata/golden/xvfb-roles.json" | |||
| 84 | + | ||||
| 85 | + | // xvfbArgs lets a run simulate a different server layout, e.g. | |||
| 86 | + | // XTURBO_XVFB_ARGS="-extension GLX" shifts resource IDs, atoms and visuals. | |||
| 87 | + | func xvfbArgs() []string { | |||
| 88 | + | args := []string{"-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac"} | |||
| 89 | + | return append(args, strings.Fields(os.Getenv("XTURBO_XVFB_ARGS"))...) | |||
| 31 | } | 90 | } | ||
| 32 | 91 | ||||
| 33 | // TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By | 92 | // TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By | ||
| @@ -43,7 +102,7 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 43 | t.Fatal(err) | 102 | t.Fatal(err) | ||
| 44 | } | 103 | } | ||
| 45 | defer r.Close() | 104 | defer r.Close() | ||
| 46 | - | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac") | 105 | + | cmd := exec.Command("Xvfb", xvfbArgs()...) |
| 47 | cmd.ExtraFiles = []*os.File{w} | 106 | cmd.ExtraFiles = []*os.File{w} | ||
| 48 | var log bytes.Buffer | 107 | var log bytes.Buffer | ||
| 49 | cmd.Stderr = &log | 108 | cmd.Stderr = &log | ||
| @@ -512,12 +571,6 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 512 | if len(events) != 10 || len(xerrors) != 3 { | 571 | if len(events) != 10 || len(xerrors) != 3 { | ||
| 513 | t.Fatalf("captured %d events, %d errors", len(events), len(xerrors)) | 572 | t.Fatalf("captured %d events, %d errors", len(events), len(xerrors)) | ||
| 514 | } | 573 | } | ||
| 515 | - | for name, p := range events { | |||
| 516 | - | golden(t, name+".event.hex", "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n", p) | |||
| 517 | - | } | |||
| 518 | - | for name, p := range xerrors { | |||
| 519 | - | golden(t, name+".error.hex", "# source: Xvfb error via raw socketpair tee to an independent reference request\n", p) | |||
| 520 | - | } | |||
| 521 | // Closing the client drains the request direction; wait before inspecting tee. | 574 | // Closing the client drains the request direction; wait before inspecting tee. | ||
| 522 | client.Close() | 575 | client.Close() | ||
| 523 | select { | 576 | select { | ||
| @@ -577,40 +630,211 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 577 | if len(captured) != 12 || len(replies) != 11 { | 630 | if len(captured) != 12 || len(replies) != 11 { | ||
| 578 | t.Fatal("missing captured requests or replies") | 631 | t.Fatal("missing captured requests or replies") | ||
| 579 | } | 632 | } | ||
| 633 | + | out := map[string][]byte{} | |||
| 580 | for name, p := range replies { | 634 | for name, p := range replies { | ||
| 581 | - | golden(t, name+".reply.hex", "# source: Xvfb reply via raw socketpair tee to an independent reference request\n", p) | 635 | + | out[name+".reply"] = p |
| 582 | } | 636 | } | ||
| 583 | for name, p := range captured { | 637 | for name, p := range captured { | ||
| 584 | - | golden(t, name+".req.hex", "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n", p) | 638 | + | out[name+".req"] = p |
| 585 | } | 639 | } | ||
| 586 | - | } | 640 | + | for name, p := range events { |
| 587 | - | 641 | + | out[name+".event"] = p | |
| 588 | - | // golden writes p (with -update) or compares it with the committed golden. | 642 | + | } |
| 589 | - | func golden(t *testing.T, name, source string, p []byte) { | 643 | + | for name, p := range xerrors { |
| 590 | - | t.Helper() | 644 | + | out[name+".error"] = p |
| 591 | - | path := filepath.Join("../../testdata/golden", name) | 645 | + | } |
| 592 | - | if !*update { | 646 | + | // Server-chosen values, keyed by the roles used in serverFields. |
| 593 | - | want, err := LoadHex(path) | 647 | + | roles := map[string]uint32{ |
| 594 | - | if err != nil { | 648 | + | "root": root, "atom": atom, "base": base, "child": child, "sibling": sibling, "picture": picture, |
| 595 | - | t.Errorf("%s: %v (run with -update to create it)", name, err) | 649 | + | "client2": client2, "badWindow": base | 0x7777, "badRegion": base | 0x8888, |
| 596 | - | return | 650 | + | "rootVisual": rootVisual, "rootColormap": binary.LittleEndian.Uint32(setup[screenOff+4:]), "rootFormat": format, |
| 651 | + | "xfixesMajor": uint32(major), "xfixesEvent": uint32(xfixesEvent), "xfixesError": uint32(xfixesError), | |||
| 652 | + | "resMajor": uint32(resMajor), "renderMajor": uint32(renderMajor), | |||
| 653 | + | } | |||
| 654 | + | if *update { | |||
| 655 | + | for name, p := range out { | |||
| 656 | + | writeGolden(t, name, p) | |||
| 597 | } | 657 | } | ||
| 598 | - | got := bytes.Clone(p) | 658 | + | rolesJSON, err := json.MarshalIndent(roles, "", " ") |
| 599 | - | if len(got) == len(want) { | 659 | + | if err != nil { |
| 600 | - | for _, r := range volatile[name] { | 660 | + | t.Fatal(err) |
| 601 | - | copy(got[r[0]:r[1]], want[r[0]:r[1]]) | |||
| 602 | - | } | |||
| 603 | } | 661 | } | ||
| 604 | - | if !bytes.Equal(got, want) { | 662 | + | if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil { |
| 605 | - | t.Errorf("%s differs from live Xvfb:\n got %x\nwant %x", name, p, want) | 663 | + | t.Fatal(err) |
| 606 | } | 664 | } | ||
| 607 | return | 665 | return | ||
| 608 | } | 666 | } | ||
| 667 | + | rolesJSON, err := os.ReadFile(rolesPath) | |||
| 668 | + | if err != nil { | |||
| 669 | + | t.Fatal(err) | |||
| 670 | + | } | |||
| 671 | + | var capturedRoles map[string]uint32 | |||
| 672 | + | if err := json.Unmarshal(rolesJSON, &capturedRoles); err != nil { | |||
| 673 | + | t.Fatal(err) | |||
| 674 | + | } | |||
| 675 | + | for _, role := range slices.Sorted(maps.Keys(roles)) { | |||
| 676 | + | if roles[role] != capturedRoles[role] { | |||
| 677 | + | t.Logf("server-chosen %s: live %#x, golden %#x", role, roles[role], capturedRoles[role]) | |||
| 678 | + | } | |||
| 679 | + | } | |||
| 680 | + | for name, p := range out { | |||
| 681 | + | compareGolden(t, name, p, roles, capturedRoles) | |||
| 682 | + | } | |||
| 683 | + | } | |||
| 684 | + | ||||
| 685 | + | var sources = map[string]string{ | |||
| 686 | + | "req": "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n", | |||
| 687 | + | "reply": "# source: Xvfb reply via raw socketpair tee to an independent reference request\n", | |||
| 688 | + | "event": "# source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request\n", | |||
| 689 | + | "error": "# source: Xvfb error via raw socketpair tee to an independent reference request\n", | |||
| 690 | + | } | |||
| 691 | + | ||||
| 692 | + | func writeGolden(t *testing.T, name string, p []byte) { | |||
| 693 | + | t.Helper() | |||
| 694 | + | path := filepath.Join("../../testdata/golden", name+".hex") | |||
| 609 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | 695 | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | ||
| 610 | t.Fatal(err) | 696 | t.Fatal(err) | ||
| 611 | } | 697 | } | ||
| 698 | + | source := sources[name[strings.LastIndexByte(name, '.')+1:]] | |||
| 612 | text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | 699 | text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | ||
| 613 | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | 700 | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | ||
| 614 | t.Fatal(err) | 701 | t.Fatal(err) | ||
| 615 | } | 702 | } | ||
| 616 | } | 703 | } | ||
| 704 | + | ||||
| 705 | + | // compareGolden checks a live packet against the committed golden without | |||
| 706 | + | // depending on the server build: server-chosen fields must hold each side's | |||
| 707 | + | // own value (live roles, recorded roles) and are masked before an exact | |||
| 708 | + | // comparison; structural replies are checked for invariants only. | |||
| 709 | + | func compareGolden(t *testing.T, name string, live []byte, roles, recorded map[string]uint32) { | |||
| 710 | + | t.Helper() | |||
| 711 | + | want, err := LoadHex(filepath.Join("../../testdata/golden", name+".hex")) | |||
| 712 | + | if err != nil { | |||
| 713 | + | t.Errorf("%s: %v (run with -update to create it)", name, err) | |||
| 714 | + | return | |||
| 715 | + | } | |||
| 716 | + | if check := structural[name]; check != nil { | |||
| 717 | + | if err := check(live, roles); err != nil { | |||
| 718 | + | t.Errorf("%s (live): %v", name, err) | |||
| 719 | + | } | |||
| 720 | + | if err := check(want, recorded); err != nil { | |||
| 721 | + | t.Errorf("%s (golden): %v", name, err) | |||
| 722 | + | } | |||
| 723 | + | return | |||
| 724 | + | } | |||
| 725 | + | got, exp, err := normalize(name, live, roles), normalize(name, want, recorded), error(nil) | |||
| 726 | + | for _, e := range []struct { | |||
| 727 | + | side string | |||
| 728 | + | err error | |||
| 729 | + | }{{"live", got.err}, {"golden", exp.err}} { | |||
| 730 | + | if e.err != nil { | |||
| 731 | + | t.Errorf("%s (%s): %v", name, e.side, e.err) | |||
| 732 | + | err = e.err | |||
| 733 | + | } | |||
| 734 | + | } | |||
| 735 | + | if err == nil && !bytes.Equal(got.b, exp.b) { | |||
| 736 | + | t.Errorf("%s differs from live Xvfb after masking server-chosen fields:\n got %x\nwant %x", name, live, want) | |||
| 737 | + | } | |||
| 738 | + | } | |||
| 739 | + | ||||
| 740 | + | type normalized struct { | |||
| 741 | + | b []byte | |||
| 742 | + | err error | |||
| 743 | + | } | |||
| 744 | + | ||||
| 745 | + | // normalize verifies each server-chosen field of p holds roles[Role] and | |||
| 746 | + | // replaces it with zero bytes. | |||
| 747 | + | func normalize(name string, p []byte, roles map[string]uint32) normalized { | |||
| 748 | + | b := bytes.Clone(p) | |||
| 749 | + | for _, f := range serverFields[name] { | |||
| 750 | + | if f.Off+f.Size > len(b) { | |||
| 751 | + | return normalized{err: fmt.Errorf("field %s at %d beyond %d bytes", f.Role, f.Off, len(b))} | |||
| 752 | + | } | |||
| 753 | + | if f.Role != "time" { | |||
| 754 | + | v, ok := roles[f.Role] | |||
| 755 | + | if !ok { | |||
| 756 | + | return normalized{err: fmt.Errorf("no recorded value for role %s", f.Role)} | |||
| 757 | + | } | |||
| 758 | + | var got uint32 | |||
| 759 | + | if f.Size == 1 { | |||
| 760 | + | got = uint32(b[f.Off]) | |||
| 761 | + | } else { | |||
| 762 | + | got = binary.LittleEndian.Uint32(b[f.Off:]) | |||
| 763 | + | } | |||
| 764 | + | if got != v { | |||
| 765 | + | return normalized{err: fmt.Errorf("field %s at %d = %#x, want %#x", f.Role, f.Off, got, v)} | |||
| 766 | + | } | |||
| 767 | + | } | |||
| 768 | + | clear(b[f.Off : f.Off+f.Size]) | |||
| 769 | + | } | |||
| 770 | + | return normalized{b: b} | |||
| 771 | + | } | |||
| 772 | + | ||||
| 773 | + | // checkListExtensions: a framed reply whose STR list holds exactly names_len | |||
| 774 | + | // entries filling the reply, including the extensions xturbo binds. | |||
| 775 | + | func checkListExtensions(p []byte, _ map[string]uint32) error { | |||
| 776 | + | if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(binary.LittleEndian.Uint32(p[4:])) { | |||
| 777 | + | return fmt.Errorf("bad framing") | |||
| 778 | + | } | |||
| 779 | + | names := map[string]bool{} | |||
| 780 | + | off := 32 | |||
| 781 | + | for range int(p[1]) { | |||
| 782 | + | if off >= len(p) || off+1+int(p[off]) > len(p) { | |||
| 783 | + | return fmt.Errorf("STR list overruns the reply") | |||
| 784 | + | } | |||
| 785 | + | names[string(p[off+1:off+1+int(p[off])])] = true | |||
| 786 | + | off += 1 + int(p[off]) | |||
| 787 | + | } | |||
| 788 | + | if (off+3)&^3 != len(p) { | |||
| 789 | + | return fmt.Errorf("STR list ends at %d, reply has %d bytes", off, len(p)) | |||
| 790 | + | } | |||
| 791 | + | for _, want := range []string{"BIG-REQUESTS", "XC-MISC", "XFIXES", "RENDER", "SHAPE", "Composite", "X-Resource", "Generic Event Extension"} { | |||
| 792 | + | if !names[want] { | |||
| 793 | + | return fmt.Errorf("missing extension %q", want) | |||
| 794 | + | } | |||
| 795 | + | } | |||
| 796 | + | return nil | |||
| 797 | + | } | |||
| 798 | + | ||||
| 799 | + | // checkQueryPictFormats: declared counts match the list sizes exactly and the | |||
| 800 | + | // root visual maps to the picture format CreatePicture used. | |||
| 801 | + | func checkQueryPictFormats(p []byte, roles map[string]uint32) error { | |||
| 802 | + | u32 := binary.LittleEndian.Uint32 | |||
| 803 | + | if len(p) < 32 || p[0] != 1 || len(p) != 32+4*int(u32(p[4:])) { | |||
| 804 | + | return fmt.Errorf("bad framing") | |||
| 805 | + | } | |||
| 806 | + | formats, screens, subpixels := int(u32(p[8:])), int(u32(p[12:])), int(u32(p[24:])) | |||
| 807 | + | off := 32 + 28*formats | |||
| 808 | + | found := false | |||
| 809 | + | for range screens { | |||
| 810 | + | if off+8 > len(p) { | |||
| 811 | + | return fmt.Errorf("screens overrun") | |||
| 812 | + | } | |||
| 813 | + | depths := int(u32(p[off:])) | |||
| 814 | + | off += 8 | |||
| 815 | + | for range depths { | |||
| 816 | + | if off+8 > len(p) { | |||
| 817 | + | return fmt.Errorf("depths overrun") | |||
| 818 | + | } | |||
| 819 | + | visuals := int(binary.LittleEndian.Uint16(p[off+2:])) | |||
| 820 | + | off += 8 | |||
| 821 | + | for range visuals { | |||
| 822 | + | if off+8 > len(p) { | |||
| 823 | + | return fmt.Errorf("visuals overrun") | |||
| 824 | + | } | |||
| 825 | + | if u32(p[off:]) == roles["rootVisual"] && u32(p[off+4:]) == roles["rootFormat"] { | |||
| 826 | + | found = true | |||
| 827 | + | } | |||
| 828 | + | off += 8 | |||
| 829 | + | } | |||
| 830 | + | } | |||
| 831 | + | } | |||
| 832 | + | off += 4 * subpixels | |||
| 833 | + | if off != len(p) { | |||
| 834 | + | return fmt.Errorf("lists end at %d, reply has %d bytes", off, len(p)) | |||
| 835 | + | } | |||
| 836 | + | if !found { | |||
| 837 | + | return fmt.Errorf("root visual %#x does not map to format %#x", roles["rootVisual"], roles["rootFormat"]) | |||
| 838 | + | } | |||
| 839 | + | return nil | |||
| 840 | + | } | |||
A testdata/golden/xvfb-roles.json +19 -0
| @@ -0,0 +1,19 @@ | |||
| 1 | + | { | |
| 2 | + | "atom": 231, | |
| 3 | + | "badRegion": 2132104, | |
| 4 | + | "badWindow": 2127735, | |
| 5 | + | "base": 2097152, | |
| 6 | + | "child": 2097153, | |
| 7 | + | "client2": 4194305, | |
| 8 | + | "picture": 2097155, | |
| 9 | + | "renderMajor": 139, | |
| 10 | + | "resMajor": 148, | |
| 11 | + | "root": 927, | |
| 12 | + | "rootColormap": 32, | |
| 13 | + | "rootFormat": 41, | |
| 14 | + | "rootVisual": 33, | |
| 15 | + | "sibling": 2097154, | |
| 16 | + | "xfixesError": 140, | |
| 17 | + | "xfixesEvent": 87, | |
| 18 | + | "xfixesMajor": 138 | |
| 19 | + | } | |
| @@ -0,0 +1,19 @@ | |||||
| 1 | + | { | |||
| 2 | + | "atom": 231, | |||
| 3 | + | "badRegion": 2132104, | |||
| 4 | + | "badWindow": 2127735, | |||
| 5 | + | "base": 2097152, | |||
| 6 | + | "child": 2097153, | |||
| 7 | + | "client2": 4194305, | |||
| 8 | + | "picture": 2097155, | |||
| 9 | + | "renderMajor": 139, | |||
| 10 | + | "resMajor": 148, | |||
| 11 | + | "root": 927, | |||
| 12 | + | "rootColormap": 32, | |||
| 13 | + | "rootFormat": 41, | |||
| 14 | + | "rootVisual": 33, | |||
| 15 | + | "sibling": 2097154, | |||
| 16 | + | "xfixesError": 140, | |||
| 17 | + | "xfixesEvent": 87, | |||
| 18 | + | "xfixesMajor": 138 | |||
| 19 | + | } | |||