#2 feat(xtgen): decode events and errors
M cmd/xtgen/emit.go +5 -0
| @@ -241,6 +241,11 @@ func emitTypes(modules []*Module) error { | |||
| 241 | 241 | var extensions []*Module | |
| 242 | 242 | for _, m := range modules { | |
| 243 | 243 | if m.ExtName != "" { | |
| 244 | + | count, err := eventCount(m) | |
| 245 | + | if err != nil { | |
| 246 | + | return err | |
| 247 | + | } | |
| 248 | + | m.NumEvents, m.NumErrors = count, errorCount(m) | |
| 244 | 249 | extensions = append(extensions, m) | |
| 245 | 250 | } | |
| 246 | 251 | } | |
| @@ -241,6 +241,11 @@ func emitTypes(modules []*Module) error { | |||||
| 241 | var extensions []*Module | 241 | var extensions []*Module | ||
| 242 | for _, m := range modules { | 242 | for _, m := range modules { | ||
| 243 | if m.ExtName != "" { | 243 | if m.ExtName != "" { | ||
| 244 | + | count, err := eventCount(m) | |||
| 245 | + | if err != nil { | |||
| 246 | + | return err | |||
| 247 | + | } | |||
| 248 | + | m.NumEvents, m.NumErrors = count, errorCount(m) | |||
| 244 | extensions = append(extensions, m) | 249 | extensions = append(extensions, m) | ||
| 245 | } | 250 | } | ||
| 246 | } | 251 | } | ||
A cmd/xtgen/events.go +434 -0
| @@ -0,0 +1,434 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "path/filepath" | |
| 6 | + | "sort" | |
| 7 | + | "strconv" | |
| 8 | + | "strings" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // eventIR is one <event> or <eventcopy>. Copies point at their source IR and | |
| 12 | + | // share its container. | |
| 13 | + | type eventIR struct { | |
| 14 | + | Name string // Go base name, e.g. MapRequest | |
| 15 | + | Code int // core: wire code; extension: relative code; XGE: evtype | |
| 16 | + | XGE bool | |
| 17 | + | Copy *eventIR | |
| 18 | + | C *container | |
| 19 | + | Node *Node | |
| 20 | + | KindExpr string // "" when the event has no Kind (core XGE envelope) | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | const eventLifetime = "Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from." | |
| 24 | + | ||
| 25 | + | func eventCode(n *Node) (int, error) { | |
| 26 | + | code, err := strconv.Atoi(n.Attr["number"]) | |
| 27 | + | if err != nil || code < 0 || code > 255 { | |
| 28 | + | return 0, n.fail("invalid event number %q", n.Attr["number"]) | |
| 29 | + | } | |
| 30 | + | return code, nil | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | func kindExpr(m *Module, e *eventIR) (string, error) { | |
| 34 | + | switch { | |
| 35 | + | case m.ExtName == "" && e.XGE: | |
| 36 | + | // The core XGE envelope is not an event type: DecodeEvent resolves | |
| 37 | + | // every XGE packet to its extension's kind or KindUnknown. | |
| 38 | + | return "", nil | |
| 39 | + | case m.ExtName == "": | |
| 40 | + | if e.Code < 2 || e.Code > 34 { | |
| 41 | + | return "", e.Node.fail("core event code %d outside 2..34", e.Code) | |
| 42 | + | } | |
| 43 | + | return fmt.Sprintf("xturbo.EventKind(%d)", e.Code), nil | |
| 44 | + | case e.XGE: | |
| 45 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | 0x80 | %d", m.ExtName, e.Code), nil | |
| 46 | + | } | |
| 47 | + | if e.Code > 127 { | |
| 48 | + | return "", e.Node.fail("extension event number %d above 127", e.Code) | |
| 49 | + | } | |
| 50 | + | if e.Code == 0 { | |
| 51 | + | // "| 0" is a no-op that staticcheck rejects (SA4016). | |
| 52 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s) << 8", m.ExtName), nil | |
| 53 | + | } | |
| 54 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | %d", m.ExtName, e.Code), nil | |
| 55 | + | } | |
| 56 | + | ||
| 57 | + | // xgeReserved are the implicit XGE header getters. | |
| 58 | + | const xgeReserved = "Raw Extension Length EventType Tail" | |
| 59 | + | ||
| 60 | + | func resolveEvents(m *Module) ([]*eventIR, error) { | |
| 61 | + | var events []*eventIR | |
| 62 | + | byXName := map[string]*eventIR{} | |
| 63 | + | for _, n := range m.Root.Children { | |
| 64 | + | if n.Kind != "event" && n.Kind != "eventcopy" { | |
| 65 | + | continue | |
| 66 | + | } | |
| 67 | + | code, err := eventCode(n) | |
| 68 | + | if err != nil { | |
| 69 | + | return nil, err | |
| 70 | + | } | |
| 71 | + | e := &eventIR{Name: goName(n.Attr["name"]), Code: code, Node: n} | |
| 72 | + | if n.Kind == "eventcopy" { | |
| 73 | + | src := byXName[n.Attr["ref"]] | |
| 74 | + | if src == nil { | |
| 75 | + | return nil, n.fail("eventcopy of unknown event %s", n.Attr["ref"]) | |
| 76 | + | } | |
| 77 | + | if src.Copy != nil { | |
| 78 | + | src = src.Copy | |
| 79 | + | } | |
| 80 | + | e.Copy, e.C, e.XGE = src, src.C, src.XGE | |
| 81 | + | } else { | |
| 82 | + | e.XGE = n.Attr["xge"] == "true" | |
| 83 | + | lay, reserved := layoutEvent, "Raw" | |
| 84 | + | switch { | |
| 85 | + | case e.XGE: | |
| 86 | + | lay, reserved = layoutXGE, xgeReserved | |
| 87 | + | if code > 127 { | |
| 88 | + | return nil, n.fail("xge evtype %d >= 128", code) | |
| 89 | + | } | |
| 90 | + | case n.Attr["no-sequence-number"] == "true": | |
| 91 | + | lay = layoutEventNoSeq | |
| 92 | + | } | |
| 93 | + | c, err := buildContainer(m, e.Name+"Event", "e", n.Children, lay, reserved) | |
| 94 | + | if err != nil { | |
| 95 | + | return nil, err | |
| 96 | + | } | |
| 97 | + | if c.Dynamic >= 0 || c.Fixed > 32 { | |
| 98 | + | return nil, n.fail("event members must lie within the first 32 bytes") | |
| 99 | + | } | |
| 100 | + | for _, mem := range c.Members { | |
| 101 | + | if mem.Kind == "list" && mem.ListKind == "iter" { | |
| 102 | + | return nil, n.fail("variable-size struct list in event") | |
| 103 | + | } | |
| 104 | + | } | |
| 105 | + | e.C = c | |
| 106 | + | } | |
| 107 | + | if e.KindExpr, err = kindExpr(m, e); err != nil { | |
| 108 | + | return nil, err | |
| 109 | + | } | |
| 110 | + | names := []string{e.Name + "Event", e.Name + "Of"} | |
| 111 | + | if e.KindExpr != "" { | |
| 112 | + | names = append(names, "Kind"+e.Name) | |
| 113 | + | } | |
| 114 | + | if m.ExtName == "" && !e.XGE { | |
| 115 | + | names = append(names, e.Name+"EventData") | |
| 116 | + | } | |
| 117 | + | for _, s := range names { | |
| 118 | + | if err := m.claim(s, n); err != nil { | |
| 119 | + | return nil, err | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | byXName[n.Attr["name"]] = e | |
| 123 | + | events = append(events, e) | |
| 124 | + | } | |
| 125 | + | return events, nil | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // dataField renders one EventData struct field and its Put statement. | |
| 129 | + | func dataField(c *container, mem member) (field, put string, err error) { | |
| 130 | + | name := mem.Method | |
| 131 | + | if name == "Put" { | |
| 132 | + | name += "_" | |
| 133 | + | } | |
| 134 | + | src := "d." + name | |
| 135 | + | at := fmt.Sprintf("tmp[%d:]", mem.Static) | |
| 136 | + | switch { | |
| 137 | + | case mem.Kind == "field" && isScalar(mem.Type): | |
| 138 | + | return name + " " + mem.GoType, putScalar(mem.Type, src, at), nil | |
| 139 | + | case mem.Kind == "field": | |
| 140 | + | return name + " " + mem.GoType, fmt.Sprintf("%s.Put(%s);", src, at), nil | |
| 141 | + | case mem.Kind == "list" && mem.Count.Kind == "value": | |
| 142 | + | count := mem.Size / mem.Type.Size | |
| 143 | + | elem := mem.Elem | |
| 144 | + | var b strings.Builder | |
| 145 | + | fmt.Fprintf(&b, "for i := range %s {", src) | |
| 146 | + | target := fmt.Sprintf("tmp[%d+i*%d:]", mem.Static, mem.Type.Size) | |
| 147 | + | if isScalar(mem.Type) { | |
| 148 | + | b.WriteString(putScalar(mem.Type, src+"[i]", target)) | |
| 149 | + | } else { | |
| 150 | + | fmt.Fprintf(&b, "%s[i].Put(%s);", src, target) | |
| 151 | + | } | |
| 152 | + | b.WriteString("};") | |
| 153 | + | return fmt.Sprintf("%s [%d]%s", name, count, elem), b.String(), nil | |
| 154 | + | } | |
| 155 | + | return "", "", c.Module.Root.fail("unsupported event member %s for EventData", mem.XName) | |
| 156 | + | } | |
| 157 | + | ||
| 158 | + | func eventDecl(m *Module, e *eventIR, lists map[string]string) (string, error) { | |
| 159 | + | var b strings.Builder | |
| 160 | + | if e.KindExpr != "" { | |
| 161 | + | fmt.Fprintf(&b, "// Kind%s identifies %s events in xturbo.Event.Kind.\nconst Kind%s = %s;\n", e.Name, e.Name, e.Name, e.KindExpr) | |
| 162 | + | } | |
| 163 | + | view := e.Name + "Event" | |
| 164 | + | if e.Copy != nil { | |
| 165 | + | src := e.Copy.Name | |
| 166 | + | fmt.Fprintf(&b, "// %s shares %s's layout (eventcopy).\ntype %s = %sEvent;\n", view, src, view, src) | |
| 167 | + | fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind.\nfunc %sOf(ev *xturbo.Event) %s { return %sOf(ev) };\n", e.Name, e.Name, e.Name, view, src) | |
| 168 | + | if m.ExtName == "" && !e.XGE { | |
| 169 | + | fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent with %s's fields.\ntype %sEventData %sEventData;\n", e.Name, e.Name, src, e.Name, src) | |
| 170 | + | fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst.\nfunc (d *%sEventData) Put(dst *[32]byte) { (*%sEventData)(d).Put(dst); dst[0] = %d };\n", e.Code, e.Name, src, e.Code) | |
| 171 | + | } | |
| 172 | + | return b.String(), nil | |
| 173 | + | } | |
| 174 | + | c := e.C | |
| 175 | + | if e.XGE { | |
| 176 | + | fmt.Fprintf(&b, "// %s is a view over a %s generic event: the copied Event.Raw plus the borrowed Event.Tail.\n// Tail is valid only until the Handler callback returns; copy what you keep.\n", view, e.Name) | |
| 177 | + | fmt.Fprintf(&b, "type %s struct { raw *[32]byte; tail []byte };\n", view) | |
| 178 | + | fmt.Fprintf(&b, "// %sOf views ev as %s; false when ev is not a generic event or Tail does not match its length.\n", e.Name, e.Name) | |
| 179 | + | fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) (%s, bool) { if ev.Raw[0]&0x7f != 35 || len(ev.Tail) != 4*int(binary.LittleEndian.Uint32(ev.Raw[4:])) { return %s{}, false }; return %s{raw: &ev.Raw, tail: ev.Tail}, true };\n", e.Name, view, view, view) | |
| 180 | + | fmt.Fprintf(&b, "func (e %s) Extension() uint8 { return e.raw[1] };\n", view) | |
| 181 | + | fmt.Fprintf(&b, "func (e %s) Length() uint32 { return binary.LittleEndian.Uint32(e.raw[4:]) };\n", view) | |
| 182 | + | fmt.Fprintf(&b, "func (e %s) EventType() uint16 { return binary.LittleEndian.Uint16(e.raw[8:]) };\n", view) | |
| 183 | + | fmt.Fprintf(&b, "func (e %s) Tail() []byte { return e.tail };\n", view) | |
| 184 | + | } else { | |
| 185 | + | fmt.Fprintf(&b, "// %s is a view over a %s event's 32 bytes. %s\n", view, e.Name, eventLifetime) | |
| 186 | + | fmt.Fprintf(&b, "type %s struct { raw *[32]byte };\n", view) | |
| 187 | + | fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind: a wrong kind yields garbage, never a panic.\n", e.Name, e.Name) | |
| 188 | + | fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) %s { return %s{raw: &ev.Raw} };\n", e.Name, view, view) | |
| 189 | + | } | |
| 190 | + | fmt.Fprintf(&b, "func (e %s) Raw() *[32]byte { return e.raw };\n", view) | |
| 191 | + | if err := c.getters(&b, view, lists); err != nil { | |
| 192 | + | return "", err | |
| 193 | + | } | |
| 194 | + | if m.ExtName != "" || e.XGE { | |
| 195 | + | return b.String(), nil | |
| 196 | + | } | |
| 197 | + | var fields, puts []string | |
| 198 | + | for _, mem := range c.Members { | |
| 199 | + | if mem.Kind == "pad" { | |
| 200 | + | continue | |
| 201 | + | } | |
| 202 | + | f, p, err := dataField(c, mem) | |
| 203 | + | if err != nil { | |
| 204 | + | return "", err | |
| 205 | + | } | |
| 206 | + | fields = append(fields, f) | |
| 207 | + | puts = append(puts, p) | |
| 208 | + | } | |
| 209 | + | fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent.\ntype %sEventData struct {\n%s\n};\n", e.Name, e.Name, e.Name, strings.Join(fields, "\n")) | |
| 210 | + | // Encode into a local array and store it last: borrowed fields (union | |
| 211 | + | // views such as ClientMessageData) may alias dst, e.g. d.Put(&ev.Raw). | |
| 212 | + | fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst. dst may alias\n// memory that d's borrowed fields view, such as the Event.Raw they were read from.\nfunc (d *%sEventData) Put(dst *[32]byte) { var tmp [32]byte; tmp[0] = %d; %s; *dst = tmp };\n", e.Code, e.Name, e.Code, strings.Join(puts, "")) | |
| 213 | + | return b.String(), nil | |
| 214 | + | } | |
| 215 | + | ||
| 216 | + | // errorIR is one <error> or <errorcopy>. | |
| 217 | + | type errorIR struct { | |
| 218 | + | Const, Name string | |
| 219 | + | Code int | |
| 220 | + | } | |
| 221 | + | ||
| 222 | + | func errorConst(xname string) string { | |
| 223 | + | return "Bad" + goName(strings.TrimPrefix(xname, "Bad")) | |
| 224 | + | } | |
| 225 | + | ||
| 226 | + | func resolveErrors(m *Module) ([]errorIR, error) { | |
| 227 | + | var errs []errorIR | |
| 228 | + | seen := map[int]bool{} | |
| 229 | + | for _, n := range m.Root.Children { | |
| 230 | + | if n.Kind != "error" && n.Kind != "errorcopy" { | |
| 231 | + | continue | |
| 232 | + | } | |
| 233 | + | code, err := strconv.Atoi(n.Attr["number"]) | |
| 234 | + | if err != nil || code < 0 || code > 255 || seen[code] { | |
| 235 | + | return nil, n.fail("invalid or duplicate error number %q", n.Attr["number"]) | |
| 236 | + | } | |
| 237 | + | seen[code] = true | |
| 238 | + | e := errorIR{Const: errorConst(n.Attr["name"]), Code: code} | |
| 239 | + | e.Name = e.Const | |
| 240 | + | if err := m.claim(e.Const, n); err != nil { | |
| 241 | + | return nil, err | |
| 242 | + | } | |
| 243 | + | errs = append(errs, e) | |
| 244 | + | } | |
| 245 | + | return errs, nil | |
| 246 | + | } | |
| 247 | + | ||
| 248 | + | // errorCount is one past the highest error number. | |
| 249 | + | func errorCount(m *Module) int { | |
| 250 | + | count := 0 | |
| 251 | + | for _, n := range m.Root.Children { | |
| 252 | + | if n.Kind == "error" || n.Kind == "errorcopy" { | |
| 253 | + | code, _ := strconv.Atoi(n.Attr["number"]) | |
| 254 | + | count = max(count, code+1) | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | return count | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | // eventCount is one past the highest non-XGE event number. | |
| 261 | + | func eventCount(m *Module) (int, error) { | |
| 262 | + | count := 0 | |
| 263 | + | for _, n := range m.Root.Children { | |
| 264 | + | if (n.Kind == "event" || n.Kind == "eventcopy") && n.Attr["xge"] != "true" { | |
| 265 | + | code, err := eventCode(n) | |
| 266 | + | if err != nil { | |
| 267 | + | return 0, err | |
| 268 | + | } | |
| 269 | + | count = max(count, code+1) | |
| 270 | + | } | |
| 271 | + | } | |
| 272 | + | return count, nil | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | func errorsDecl(m *Module, errs []errorIR) string { | |
| 276 | + | var b strings.Builder | |
| 277 | + | what := "wire error codes" | |
| 278 | + | if m.ExtName != "" { | |
| 279 | + | what = "error codes relative to the extension's FirstError (XError.Rel)" | |
| 280 | + | } | |
| 281 | + | fmt.Fprintf(&b, "// Error codes: %s.\nconst (\n", what) | |
| 282 | + | for _, e := range errs { | |
| 283 | + | fmt.Fprintf(&b, "%s uint8 = %d\n", e.Const, e.Code) | |
| 284 | + | } | |
| 285 | + | b.WriteString(");\n") | |
| 286 | + | names := make([]string, errorCount(m)) | |
| 287 | + | for _, e := range errs { | |
| 288 | + | names[e.Code] = e.Name | |
| 289 | + | } | |
| 290 | + | b.WriteString("var errorNames = [...]string{") | |
| 291 | + | for _, n := range names { | |
| 292 | + | fmt.Fprintf(&b, "%q,", n) | |
| 293 | + | } | |
| 294 | + | b.WriteString("};\n") | |
| 295 | + | fmt.Fprintf(&b, "// ErrorName returns the name of an error code (%s), or \"\" when unknown.\n", what) | |
| 296 | + | b.WriteString("func ErrorName(code uint8) string { if int(code) < len(errorNames) { return errorNames[code] }; return \"\" };\n") | |
| 297 | + | return b.String() | |
| 298 | + | } | |
| 299 | + | ||
| 300 | + | func emitEvents(modules []*Module) error { | |
| 301 | + | for _, m := range modules { | |
| 302 | + | events, err := resolveEvents(m) | |
| 303 | + | if err != nil { | |
| 304 | + | return err | |
| 305 | + | } | |
| 306 | + | lists := map[string]string{} | |
| 307 | + | var decls []string | |
| 308 | + | for _, e := range events { | |
| 309 | + | d, err := eventDecl(m, e, lists) | |
| 310 | + | if err != nil { | |
| 311 | + | return err | |
| 312 | + | } | |
| 313 | + | decls = append(decls, d) | |
| 314 | + | } | |
| 315 | + | var names []string | |
| 316 | + | for name := range lists { | |
| 317 | + | // Reuse a list view already declared by replies_generated.go. | |
| 318 | + | if old := m.Names[name]; old != nil && old == m.Root { | |
| 319 | + | continue | |
| 320 | + | } | |
| 321 | + | if err := m.claim(name, m.Root); err != nil { | |
| 322 | + | return err | |
| 323 | + | } | |
| 324 | + | names = append(names, name) | |
| 325 | + | } | |
| 326 | + | sort.Strings(names) | |
| 327 | + | for _, name := range names { | |
| 328 | + | decls = append(decls, lists[name]) | |
| 329 | + | } | |
| 330 | + | errs, err := resolveErrors(m) | |
| 331 | + | if err != nil { | |
| 332 | + | return err | |
| 333 | + | } | |
| 334 | + | if len(errs) > 0 { | |
| 335 | + | data := struct { | |
| 336 | + | Pkg string | |
| 337 | + | Decls []string | |
| 338 | + | }{Pkg: m.Pkg, Decls: []string{errorsDecl(m, errs)}} | |
| 339 | + | if err := writeTemplate(filepath.Join(m.Pkg, "errors_generated.go"), "errors", data); err != nil { | |
| 340 | + | return err | |
| 341 | + | } | |
| 342 | + | } | |
| 343 | + | if len(decls) == 0 { | |
| 344 | + | continue | |
| 345 | + | } | |
| 346 | + | imports := importsFor(m, strings.Join(decls, "\n")) + "\nimport \"bnema.dev/xturbo\"" | |
| 347 | + | decls = append([]string{imports}, decls...) | |
| 348 | + | data := struct { | |
| 349 | + | Pkg string | |
| 350 | + | Decls []string | |
| 351 | + | }{Pkg: m.Pkg, Decls: decls} | |
| 352 | + | if err := writeTemplate(filepath.Join(m.Pkg, "events_generated.go"), "events", data); err != nil { | |
| 353 | + | return err | |
| 354 | + | } | |
| 355 | + | if err := emitEventFuzz(m, events); err != nil { | |
| 356 | + | return err | |
| 357 | + | } | |
| 358 | + | } | |
| 359 | + | return nil | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | // emitEventFuzz writes FuzzEventViews: every view getter runs on arbitrary | |
| 363 | + | // bytes, and for core events EventData built from the getters survives a | |
| 364 | + | // Put/view round trip. Seeds are the package's event goldens. | |
| 365 | + | func emitEventFuzz(m *Module, events []*eventIR) error { | |
| 366 | + | var b strings.Builder | |
| 367 | + | b.WriteString("func FuzzEventViews(f *testing.F) {\nf.Add(make([]byte, 32));\n") | |
| 368 | + | goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, "*.event.hex")) | |
| 369 | + | sort.Strings(goldens) | |
| 370 | + | for _, g := range goldens { | |
| 371 | + | fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g)) | |
| 372 | + | } | |
| 373 | + | b.WriteString("f.Fuzz(func(t *testing.T, raw []byte) {\nif len(raw) < 32 { return };\nvar ev xturbo.Event;\ncopy(ev.Raw[:], raw);\nev.Tail = raw[32:];\n") | |
| 374 | + | for _, e := range events { | |
| 375 | + | if e.Copy != nil { | |
| 376 | + | fmt.Fprintf(&b, "_ = %sOf(&ev);\n", e.Name) | |
| 377 | + | continue | |
| 378 | + | } | |
| 379 | + | v := "v" + e.Name | |
| 380 | + | if e.XGE { | |
| 381 | + | fmt.Fprintf(&b, "if %s, ok := %sOf(&ev); ok { _ = %s.Extension(); _ = %s.Length(); _ = %s.EventType(); _ = len(%s.Tail());", v, e.Name, v, v, v, v) | |
| 382 | + | } else { | |
| 383 | + | fmt.Fprintf(&b, "{ %s := %sOf(&ev);", v, e.Name) | |
| 384 | + | } | |
| 385 | + | touch(&b, e.C, v, 0, nil, `t.Fatal("iterator error")`) | |
| 386 | + | if m.ExtName == "" && !e.XGE { | |
| 387 | + | var fields, checks []string | |
| 388 | + | for _, mem := range e.C.Members { | |
| 389 | + | if mem.Kind == "pad" { | |
| 390 | + | continue | |
| 391 | + | } | |
| 392 | + | name := mem.Method | |
| 393 | + | if name == "Put" { | |
| 394 | + | name += "_" | |
| 395 | + | } | |
| 396 | + | switch { | |
| 397 | + | case mem.Kind == "list" && mem.ListKind == "bytes": | |
| 398 | + | fields = append(fields, fmt.Sprintf("%s: [%d]%s(%s.%s())", name, mem.Size/mem.Type.Size, mem.Elem, v, mem.Method)) | |
| 399 | + | checks = append(checks, fmt.Sprintf("if !bytes.Equal(w.%s(), %s.%s()) { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) | |
| 400 | + | case mem.Kind == "field" && isScalar(mem.Type): | |
| 401 | + | fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) | |
| 402 | + | checks = append(checks, fmt.Sprintf("if w.%s() != %s.%s() { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) | |
| 403 | + | case mem.Kind == "field": | |
| 404 | + | fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) | |
| 405 | + | checks = append(checks, fmt.Sprintf("if !bytes.Equal(out[%d:%d], ev.Raw[%d:%d]) { t.Fatal(%q) };", mem.Static, mem.Static+mem.Size, mem.Static, mem.Static+mem.Size, e.Name+"."+mem.Method)) | |
| 406 | + | default: | |
| 407 | + | return e.Node.fail("event member %s has no EventData round trip", mem.XName) | |
| 408 | + | } | |
| 409 | + | } | |
| 410 | + | fmt.Fprintf(&b, "d := %sEventData{%s}; var out [32]byte; d.Put(&out);", e.Name, strings.Join(fields, ", ")) | |
| 411 | + | fmt.Fprintf(&b, "if out[0] != %d { t.Fatal(\"%s code\") };", e.Code, e.Name) | |
| 412 | + | b.WriteString("back := xturbo.Event{Raw: out};") | |
| 413 | + | fmt.Fprintf(&b, "w := %sOf(&back); %s", e.Name, strings.Join(checks, "")) | |
| 414 | + | // In place: dst aliases the Raw that d's borrowed fields view. | |
| 415 | + | fmt.Fprintf(&b, "in := xturbo.Event{Raw: ev.Raw}; vi := %sOf(&in); di := %sEventData{%s}; di.Put(&in.Raw); if in.Raw != out { t.Fatal(%q) };", | |
| 416 | + | e.Name, e.Name, strings.ReplaceAll(strings.Join(fields, ", "), v+".", "vi."), e.Name+" in-place Put") | |
| 417 | + | } | |
| 418 | + | b.WriteString("};\n") | |
| 419 | + | } | |
| 420 | + | b.WriteString("})\n}\n") | |
| 421 | + | body := b.String() | |
| 422 | + | imports := "import \"testing\"\nimport \"bnema.dev/xturbo\"" | |
| 423 | + | if len(goldens) > 0 { | |
| 424 | + | imports += "\nimport \"bnema.dev/xturbo/internal/xtest\"" | |
| 425 | + | } | |
| 426 | + | if strings.Contains(body, "bytes.") { | |
| 427 | + | imports += "\nimport \"bytes\"" | |
| 428 | + | } | |
| 429 | + | data := struct { | |
| 430 | + | Pkg string | |
| 431 | + | Decls []string | |
| 432 | + | }{Pkg: m.Pkg, Decls: []string{imports, body}} | |
| 433 | + | return writeTemplate(filepath.Join(m.Pkg, "events_fuzz_test.go"), "events", data) | |
| 434 | + | } | |
| @@ -0,0 +1,434 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "path/filepath" | |||
| 6 | + | "sort" | |||
| 7 | + | "strconv" | |||
| 8 | + | "strings" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // eventIR is one <event> or <eventcopy>. Copies point at their source IR and | |||
| 12 | + | // share its container. | |||
| 13 | + | type eventIR struct { | |||
| 14 | + | Name string // Go base name, e.g. MapRequest | |||
| 15 | + | Code int // core: wire code; extension: relative code; XGE: evtype | |||
| 16 | + | XGE bool | |||
| 17 | + | Copy *eventIR | |||
| 18 | + | C *container | |||
| 19 | + | Node *Node | |||
| 20 | + | KindExpr string // "" when the event has no Kind (core XGE envelope) | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | const eventLifetime = "Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from." | |||
| 24 | + | ||||
| 25 | + | func eventCode(n *Node) (int, error) { | |||
| 26 | + | code, err := strconv.Atoi(n.Attr["number"]) | |||
| 27 | + | if err != nil || code < 0 || code > 255 { | |||
| 28 | + | return 0, n.fail("invalid event number %q", n.Attr["number"]) | |||
| 29 | + | } | |||
| 30 | + | return code, nil | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | func kindExpr(m *Module, e *eventIR) (string, error) { | |||
| 34 | + | switch { | |||
| 35 | + | case m.ExtName == "" && e.XGE: | |||
| 36 | + | // The core XGE envelope is not an event type: DecodeEvent resolves | |||
| 37 | + | // every XGE packet to its extension's kind or KindUnknown. | |||
| 38 | + | return "", nil | |||
| 39 | + | case m.ExtName == "": | |||
| 40 | + | if e.Code < 2 || e.Code > 34 { | |||
| 41 | + | return "", e.Node.fail("core event code %d outside 2..34", e.Code) | |||
| 42 | + | } | |||
| 43 | + | return fmt.Sprintf("xturbo.EventKind(%d)", e.Code), nil | |||
| 44 | + | case e.XGE: | |||
| 45 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | 0x80 | %d", m.ExtName, e.Code), nil | |||
| 46 | + | } | |||
| 47 | + | if e.Code > 127 { | |||
| 48 | + | return "", e.Node.fail("extension event number %d above 127", e.Code) | |||
| 49 | + | } | |||
| 50 | + | if e.Code == 0 { | |||
| 51 | + | // "| 0" is a no-op that staticcheck rejects (SA4016). | |||
| 52 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s) << 8", m.ExtName), nil | |||
| 53 | + | } | |||
| 54 | + | return fmt.Sprintf("xturbo.EventKind(xturbo.Ext%s)<<8 | %d", m.ExtName, e.Code), nil | |||
| 55 | + | } | |||
| 56 | + | ||||
| 57 | + | // xgeReserved are the implicit XGE header getters. | |||
| 58 | + | const xgeReserved = "Raw Extension Length EventType Tail" | |||
| 59 | + | ||||
| 60 | + | func resolveEvents(m *Module) ([]*eventIR, error) { | |||
| 61 | + | var events []*eventIR | |||
| 62 | + | byXName := map[string]*eventIR{} | |||
| 63 | + | for _, n := range m.Root.Children { | |||
| 64 | + | if n.Kind != "event" && n.Kind != "eventcopy" { | |||
| 65 | + | continue | |||
| 66 | + | } | |||
| 67 | + | code, err := eventCode(n) | |||
| 68 | + | if err != nil { | |||
| 69 | + | return nil, err | |||
| 70 | + | } | |||
| 71 | + | e := &eventIR{Name: goName(n.Attr["name"]), Code: code, Node: n} | |||
| 72 | + | if n.Kind == "eventcopy" { | |||
| 73 | + | src := byXName[n.Attr["ref"]] | |||
| 74 | + | if src == nil { | |||
| 75 | + | return nil, n.fail("eventcopy of unknown event %s", n.Attr["ref"]) | |||
| 76 | + | } | |||
| 77 | + | if src.Copy != nil { | |||
| 78 | + | src = src.Copy | |||
| 79 | + | } | |||
| 80 | + | e.Copy, e.C, e.XGE = src, src.C, src.XGE | |||
| 81 | + | } else { | |||
| 82 | + | e.XGE = n.Attr["xge"] == "true" | |||
| 83 | + | lay, reserved := layoutEvent, "Raw" | |||
| 84 | + | switch { | |||
| 85 | + | case e.XGE: | |||
| 86 | + | lay, reserved = layoutXGE, xgeReserved | |||
| 87 | + | if code > 127 { | |||
| 88 | + | return nil, n.fail("xge evtype %d >= 128", code) | |||
| 89 | + | } | |||
| 90 | + | case n.Attr["no-sequence-number"] == "true": | |||
| 91 | + | lay = layoutEventNoSeq | |||
| 92 | + | } | |||
| 93 | + | c, err := buildContainer(m, e.Name+"Event", "e", n.Children, lay, reserved) | |||
| 94 | + | if err != nil { | |||
| 95 | + | return nil, err | |||
| 96 | + | } | |||
| 97 | + | if c.Dynamic >= 0 || c.Fixed > 32 { | |||
| 98 | + | return nil, n.fail("event members must lie within the first 32 bytes") | |||
| 99 | + | } | |||
| 100 | + | for _, mem := range c.Members { | |||
| 101 | + | if mem.Kind == "list" && mem.ListKind == "iter" { | |||
| 102 | + | return nil, n.fail("variable-size struct list in event") | |||
| 103 | + | } | |||
| 104 | + | } | |||
| 105 | + | e.C = c | |||
| 106 | + | } | |||
| 107 | + | if e.KindExpr, err = kindExpr(m, e); err != nil { | |||
| 108 | + | return nil, err | |||
| 109 | + | } | |||
| 110 | + | names := []string{e.Name + "Event", e.Name + "Of"} | |||
| 111 | + | if e.KindExpr != "" { | |||
| 112 | + | names = append(names, "Kind"+e.Name) | |||
| 113 | + | } | |||
| 114 | + | if m.ExtName == "" && !e.XGE { | |||
| 115 | + | names = append(names, e.Name+"EventData") | |||
| 116 | + | } | |||
| 117 | + | for _, s := range names { | |||
| 118 | + | if err := m.claim(s, n); err != nil { | |||
| 119 | + | return nil, err | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | byXName[n.Attr["name"]] = e | |||
| 123 | + | events = append(events, e) | |||
| 124 | + | } | |||
| 125 | + | return events, nil | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // dataField renders one EventData struct field and its Put statement. | |||
| 129 | + | func dataField(c *container, mem member) (field, put string, err error) { | |||
| 130 | + | name := mem.Method | |||
| 131 | + | if name == "Put" { | |||
| 132 | + | name += "_" | |||
| 133 | + | } | |||
| 134 | + | src := "d." + name | |||
| 135 | + | at := fmt.Sprintf("tmp[%d:]", mem.Static) | |||
| 136 | + | switch { | |||
| 137 | + | case mem.Kind == "field" && isScalar(mem.Type): | |||
| 138 | + | return name + " " + mem.GoType, putScalar(mem.Type, src, at), nil | |||
| 139 | + | case mem.Kind == "field": | |||
| 140 | + | return name + " " + mem.GoType, fmt.Sprintf("%s.Put(%s);", src, at), nil | |||
| 141 | + | case mem.Kind == "list" && mem.Count.Kind == "value": | |||
| 142 | + | count := mem.Size / mem.Type.Size | |||
| 143 | + | elem := mem.Elem | |||
| 144 | + | var b strings.Builder | |||
| 145 | + | fmt.Fprintf(&b, "for i := range %s {", src) | |||
| 146 | + | target := fmt.Sprintf("tmp[%d+i*%d:]", mem.Static, mem.Type.Size) | |||
| 147 | + | if isScalar(mem.Type) { | |||
| 148 | + | b.WriteString(putScalar(mem.Type, src+"[i]", target)) | |||
| 149 | + | } else { | |||
| 150 | + | fmt.Fprintf(&b, "%s[i].Put(%s);", src, target) | |||
| 151 | + | } | |||
| 152 | + | b.WriteString("};") | |||
| 153 | + | return fmt.Sprintf("%s [%d]%s", name, count, elem), b.String(), nil | |||
| 154 | + | } | |||
| 155 | + | return "", "", c.Module.Root.fail("unsupported event member %s for EventData", mem.XName) | |||
| 156 | + | } | |||
| 157 | + | ||||
| 158 | + | func eventDecl(m *Module, e *eventIR, lists map[string]string) (string, error) { | |||
| 159 | + | var b strings.Builder | |||
| 160 | + | if e.KindExpr != "" { | |||
| 161 | + | fmt.Fprintf(&b, "// Kind%s identifies %s events in xturbo.Event.Kind.\nconst Kind%s = %s;\n", e.Name, e.Name, e.Name, e.KindExpr) | |||
| 162 | + | } | |||
| 163 | + | view := e.Name + "Event" | |||
| 164 | + | if e.Copy != nil { | |||
| 165 | + | src := e.Copy.Name | |||
| 166 | + | fmt.Fprintf(&b, "// %s shares %s's layout (eventcopy).\ntype %s = %sEvent;\n", view, src, view, src) | |||
| 167 | + | fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind.\nfunc %sOf(ev *xturbo.Event) %s { return %sOf(ev) };\n", e.Name, e.Name, e.Name, view, src) | |||
| 168 | + | if m.ExtName == "" && !e.XGE { | |||
| 169 | + | fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent with %s's fields.\ntype %sEventData %sEventData;\n", e.Name, e.Name, src, e.Name, src) | |||
| 170 | + | fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst.\nfunc (d *%sEventData) Put(dst *[32]byte) { (*%sEventData)(d).Put(dst); dst[0] = %d };\n", e.Code, e.Name, src, e.Code) | |||
| 171 | + | } | |||
| 172 | + | return b.String(), nil | |||
| 173 | + | } | |||
| 174 | + | c := e.C | |||
| 175 | + | if e.XGE { | |||
| 176 | + | fmt.Fprintf(&b, "// %s is a view over a %s generic event: the copied Event.Raw plus the borrowed Event.Tail.\n// Tail is valid only until the Handler callback returns; copy what you keep.\n", view, e.Name) | |||
| 177 | + | fmt.Fprintf(&b, "type %s struct { raw *[32]byte; tail []byte };\n", view) | |||
| 178 | + | fmt.Fprintf(&b, "// %sOf views ev as %s; false when ev is not a generic event or Tail does not match its length.\n", e.Name, e.Name) | |||
| 179 | + | fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) (%s, bool) { if ev.Raw[0]&0x7f != 35 || len(ev.Tail) != 4*int(binary.LittleEndian.Uint32(ev.Raw[4:])) { return %s{}, false }; return %s{raw: &ev.Raw, tail: ev.Tail}, true };\n", e.Name, view, view, view) | |||
| 180 | + | fmt.Fprintf(&b, "func (e %s) Extension() uint8 { return e.raw[1] };\n", view) | |||
| 181 | + | fmt.Fprintf(&b, "func (e %s) Length() uint32 { return binary.LittleEndian.Uint32(e.raw[4:]) };\n", view) | |||
| 182 | + | fmt.Fprintf(&b, "func (e %s) EventType() uint16 { return binary.LittleEndian.Uint16(e.raw[8:]) };\n", view) | |||
| 183 | + | fmt.Fprintf(&b, "func (e %s) Tail() []byte { return e.tail };\n", view) | |||
| 184 | + | } else { | |||
| 185 | + | fmt.Fprintf(&b, "// %s is a view over a %s event's 32 bytes. %s\n", view, e.Name, eventLifetime) | |||
| 186 | + | fmt.Fprintf(&b, "type %s struct { raw *[32]byte };\n", view) | |||
| 187 | + | fmt.Fprintf(&b, "// %sOf views ev as %s without checking ev.Kind: a wrong kind yields garbage, never a panic.\n", e.Name, e.Name) | |||
| 188 | + | fmt.Fprintf(&b, "func %sOf(ev *xturbo.Event) %s { return %s{raw: &ev.Raw} };\n", e.Name, view, view) | |||
| 189 | + | } | |||
| 190 | + | fmt.Fprintf(&b, "func (e %s) Raw() *[32]byte { return e.raw };\n", view) | |||
| 191 | + | if err := c.getters(&b, view, lists); err != nil { | |||
| 192 | + | return "", err | |||
| 193 | + | } | |||
| 194 | + | if m.ExtName != "" || e.XGE { | |||
| 195 | + | return b.String(), nil | |||
| 196 | + | } | |||
| 197 | + | var fields, puts []string | |||
| 198 | + | for _, mem := range c.Members { | |||
| 199 | + | if mem.Kind == "pad" { | |||
| 200 | + | continue | |||
| 201 | + | } | |||
| 202 | + | f, p, err := dataField(c, mem) | |||
| 203 | + | if err != nil { | |||
| 204 | + | return "", err | |||
| 205 | + | } | |||
| 206 | + | fields = append(fields, f) | |||
| 207 | + | puts = append(puts, p) | |||
| 208 | + | } | |||
| 209 | + | fmt.Fprintf(&b, "// %sEventData encodes a %s event for SendEvent.\ntype %sEventData struct {\n%s\n};\n", e.Name, e.Name, e.Name, strings.Join(fields, "\n")) | |||
| 210 | + | // Encode into a local array and store it last: borrowed fields (union | |||
| 211 | + | // views such as ClientMessageData) may alias dst, e.g. d.Put(&ev.Raw). | |||
| 212 | + | fmt.Fprintf(&b, "// Put writes the event (code %d, pads and sequence zeroed) into dst. dst may alias\n// memory that d's borrowed fields view, such as the Event.Raw they were read from.\nfunc (d *%sEventData) Put(dst *[32]byte) { var tmp [32]byte; tmp[0] = %d; %s; *dst = tmp };\n", e.Code, e.Name, e.Code, strings.Join(puts, "")) | |||
| 213 | + | return b.String(), nil | |||
| 214 | + | } | |||
| 215 | + | ||||
| 216 | + | // errorIR is one <error> or <errorcopy>. | |||
| 217 | + | type errorIR struct { | |||
| 218 | + | Const, Name string | |||
| 219 | + | Code int | |||
| 220 | + | } | |||
| 221 | + | ||||
| 222 | + | func errorConst(xname string) string { | |||
| 223 | + | return "Bad" + goName(strings.TrimPrefix(xname, "Bad")) | |||
| 224 | + | } | |||
| 225 | + | ||||
| 226 | + | func resolveErrors(m *Module) ([]errorIR, error) { | |||
| 227 | + | var errs []errorIR | |||
| 228 | + | seen := map[int]bool{} | |||
| 229 | + | for _, n := range m.Root.Children { | |||
| 230 | + | if n.Kind != "error" && n.Kind != "errorcopy" { | |||
| 231 | + | continue | |||
| 232 | + | } | |||
| 233 | + | code, err := strconv.Atoi(n.Attr["number"]) | |||
| 234 | + | if err != nil || code < 0 || code > 255 || seen[code] { | |||
| 235 | + | return nil, n.fail("invalid or duplicate error number %q", n.Attr["number"]) | |||
| 236 | + | } | |||
| 237 | + | seen[code] = true | |||
| 238 | + | e := errorIR{Const: errorConst(n.Attr["name"]), Code: code} | |||
| 239 | + | e.Name = e.Const | |||
| 240 | + | if err := m.claim(e.Const, n); err != nil { | |||
| 241 | + | return nil, err | |||
| 242 | + | } | |||
| 243 | + | errs = append(errs, e) | |||
| 244 | + | } | |||
| 245 | + | return errs, nil | |||
| 246 | + | } | |||
| 247 | + | ||||
| 248 | + | // errorCount is one past the highest error number. | |||
| 249 | + | func errorCount(m *Module) int { | |||
| 250 | + | count := 0 | |||
| 251 | + | for _, n := range m.Root.Children { | |||
| 252 | + | if n.Kind == "error" || n.Kind == "errorcopy" { | |||
| 253 | + | code, _ := strconv.Atoi(n.Attr["number"]) | |||
| 254 | + | count = max(count, code+1) | |||
| 255 | + | } | |||
| 256 | + | } | |||
| 257 | + | return count | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // eventCount is one past the highest non-XGE event number. | |||
| 261 | + | func eventCount(m *Module) (int, error) { | |||
| 262 | + | count := 0 | |||
| 263 | + | for _, n := range m.Root.Children { | |||
| 264 | + | if (n.Kind == "event" || n.Kind == "eventcopy") && n.Attr["xge"] != "true" { | |||
| 265 | + | code, err := eventCode(n) | |||
| 266 | + | if err != nil { | |||
| 267 | + | return 0, err | |||
| 268 | + | } | |||
| 269 | + | count = max(count, code+1) | |||
| 270 | + | } | |||
| 271 | + | } | |||
| 272 | + | return count, nil | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | func errorsDecl(m *Module, errs []errorIR) string { | |||
| 276 | + | var b strings.Builder | |||
| 277 | + | what := "wire error codes" | |||
| 278 | + | if m.ExtName != "" { | |||
| 279 | + | what = "error codes relative to the extension's FirstError (XError.Rel)" | |||
| 280 | + | } | |||
| 281 | + | fmt.Fprintf(&b, "// Error codes: %s.\nconst (\n", what) | |||
| 282 | + | for _, e := range errs { | |||
| 283 | + | fmt.Fprintf(&b, "%s uint8 = %d\n", e.Const, e.Code) | |||
| 284 | + | } | |||
| 285 | + | b.WriteString(");\n") | |||
| 286 | + | names := make([]string, errorCount(m)) | |||
| 287 | + | for _, e := range errs { | |||
| 288 | + | names[e.Code] = e.Name | |||
| 289 | + | } | |||
| 290 | + | b.WriteString("var errorNames = [...]string{") | |||
| 291 | + | for _, n := range names { | |||
| 292 | + | fmt.Fprintf(&b, "%q,", n) | |||
| 293 | + | } | |||
| 294 | + | b.WriteString("};\n") | |||
| 295 | + | fmt.Fprintf(&b, "// ErrorName returns the name of an error code (%s), or \"\" when unknown.\n", what) | |||
| 296 | + | b.WriteString("func ErrorName(code uint8) string { if int(code) < len(errorNames) { return errorNames[code] }; return \"\" };\n") | |||
| 297 | + | return b.String() | |||
| 298 | + | } | |||
| 299 | + | ||||
| 300 | + | func emitEvents(modules []*Module) error { | |||
| 301 | + | for _, m := range modules { | |||
| 302 | + | events, err := resolveEvents(m) | |||
| 303 | + | if err != nil { | |||
| 304 | + | return err | |||
| 305 | + | } | |||
| 306 | + | lists := map[string]string{} | |||
| 307 | + | var decls []string | |||
| 308 | + | for _, e := range events { | |||
| 309 | + | d, err := eventDecl(m, e, lists) | |||
| 310 | + | if err != nil { | |||
| 311 | + | return err | |||
| 312 | + | } | |||
| 313 | + | decls = append(decls, d) | |||
| 314 | + | } | |||
| 315 | + | var names []string | |||
| 316 | + | for name := range lists { | |||
| 317 | + | // Reuse a list view already declared by replies_generated.go. | |||
| 318 | + | if old := m.Names[name]; old != nil && old == m.Root { | |||
| 319 | + | continue | |||
| 320 | + | } | |||
| 321 | + | if err := m.claim(name, m.Root); err != nil { | |||
| 322 | + | return err | |||
| 323 | + | } | |||
| 324 | + | names = append(names, name) | |||
| 325 | + | } | |||
| 326 | + | sort.Strings(names) | |||
| 327 | + | for _, name := range names { | |||
| 328 | + | decls = append(decls, lists[name]) | |||
| 329 | + | } | |||
| 330 | + | errs, err := resolveErrors(m) | |||
| 331 | + | if err != nil { | |||
| 332 | + | return err | |||
| 333 | + | } | |||
| 334 | + | if len(errs) > 0 { | |||
| 335 | + | data := struct { | |||
| 336 | + | Pkg string | |||
| 337 | + | Decls []string | |||
| 338 | + | }{Pkg: m.Pkg, Decls: []string{errorsDecl(m, errs)}} | |||
| 339 | + | if err := writeTemplate(filepath.Join(m.Pkg, "errors_generated.go"), "errors", data); err != nil { | |||
| 340 | + | return err | |||
| 341 | + | } | |||
| 342 | + | } | |||
| 343 | + | if len(decls) == 0 { | |||
| 344 | + | continue | |||
| 345 | + | } | |||
| 346 | + | imports := importsFor(m, strings.Join(decls, "\n")) + "\nimport \"bnema.dev/xturbo\"" | |||
| 347 | + | decls = append([]string{imports}, decls...) | |||
| 348 | + | data := struct { | |||
| 349 | + | Pkg string | |||
| 350 | + | Decls []string | |||
| 351 | + | }{Pkg: m.Pkg, Decls: decls} | |||
| 352 | + | if err := writeTemplate(filepath.Join(m.Pkg, "events_generated.go"), "events", data); err != nil { | |||
| 353 | + | return err | |||
| 354 | + | } | |||
| 355 | + | if err := emitEventFuzz(m, events); err != nil { | |||
| 356 | + | return err | |||
| 357 | + | } | |||
| 358 | + | } | |||
| 359 | + | return nil | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | // emitEventFuzz writes FuzzEventViews: every view getter runs on arbitrary | |||
| 363 | + | // bytes, and for core events EventData built from the getters survives a | |||
| 364 | + | // Put/view round trip. Seeds are the package's event goldens. | |||
| 365 | + | func emitEventFuzz(m *Module, events []*eventIR) error { | |||
| 366 | + | var b strings.Builder | |||
| 367 | + | b.WriteString("func FuzzEventViews(f *testing.F) {\nf.Add(make([]byte, 32));\n") | |||
| 368 | + | goldens, _ := filepath.Glob(filepath.Join("testdata/golden", m.Pkg, "*.event.hex")) | |||
| 369 | + | sort.Strings(goldens) | |||
| 370 | + | for _, g := range goldens { | |||
| 371 | + | fmt.Fprintf(&b, "if raw, err := xtest.LoadHex(%q); err != nil { f.Fatal(err) } else { f.Add(raw) };\n", "../"+filepath.ToSlash(g)) | |||
| 372 | + | } | |||
| 373 | + | b.WriteString("f.Fuzz(func(t *testing.T, raw []byte) {\nif len(raw) < 32 { return };\nvar ev xturbo.Event;\ncopy(ev.Raw[:], raw);\nev.Tail = raw[32:];\n") | |||
| 374 | + | for _, e := range events { | |||
| 375 | + | if e.Copy != nil { | |||
| 376 | + | fmt.Fprintf(&b, "_ = %sOf(&ev);\n", e.Name) | |||
| 377 | + | continue | |||
| 378 | + | } | |||
| 379 | + | v := "v" + e.Name | |||
| 380 | + | if e.XGE { | |||
| 381 | + | fmt.Fprintf(&b, "if %s, ok := %sOf(&ev); ok { _ = %s.Extension(); _ = %s.Length(); _ = %s.EventType(); _ = len(%s.Tail());", v, e.Name, v, v, v, v) | |||
| 382 | + | } else { | |||
| 383 | + | fmt.Fprintf(&b, "{ %s := %sOf(&ev);", v, e.Name) | |||
| 384 | + | } | |||
| 385 | + | touch(&b, e.C, v, 0, nil, `t.Fatal("iterator error")`) | |||
| 386 | + | if m.ExtName == "" && !e.XGE { | |||
| 387 | + | var fields, checks []string | |||
| 388 | + | for _, mem := range e.C.Members { | |||
| 389 | + | if mem.Kind == "pad" { | |||
| 390 | + | continue | |||
| 391 | + | } | |||
| 392 | + | name := mem.Method | |||
| 393 | + | if name == "Put" { | |||
| 394 | + | name += "_" | |||
| 395 | + | } | |||
| 396 | + | switch { | |||
| 397 | + | case mem.Kind == "list" && mem.ListKind == "bytes": | |||
| 398 | + | fields = append(fields, fmt.Sprintf("%s: [%d]%s(%s.%s())", name, mem.Size/mem.Type.Size, mem.Elem, v, mem.Method)) | |||
| 399 | + | checks = append(checks, fmt.Sprintf("if !bytes.Equal(w.%s(), %s.%s()) { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) | |||
| 400 | + | case mem.Kind == "field" && isScalar(mem.Type): | |||
| 401 | + | fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) | |||
| 402 | + | checks = append(checks, fmt.Sprintf("if w.%s() != %s.%s() { t.Fatal(%q) };", mem.Method, v, mem.Method, e.Name+"."+mem.Method)) | |||
| 403 | + | case mem.Kind == "field": | |||
| 404 | + | fields = append(fields, fmt.Sprintf("%s: %s.%s()", name, v, mem.Method)) | |||
| 405 | + | checks = append(checks, fmt.Sprintf("if !bytes.Equal(out[%d:%d], ev.Raw[%d:%d]) { t.Fatal(%q) };", mem.Static, mem.Static+mem.Size, mem.Static, mem.Static+mem.Size, e.Name+"."+mem.Method)) | |||
| 406 | + | default: | |||
| 407 | + | return e.Node.fail("event member %s has no EventData round trip", mem.XName) | |||
| 408 | + | } | |||
| 409 | + | } | |||
| 410 | + | fmt.Fprintf(&b, "d := %sEventData{%s}; var out [32]byte; d.Put(&out);", e.Name, strings.Join(fields, ", ")) | |||
| 411 | + | fmt.Fprintf(&b, "if out[0] != %d { t.Fatal(\"%s code\") };", e.Code, e.Name) | |||
| 412 | + | b.WriteString("back := xturbo.Event{Raw: out};") | |||
| 413 | + | fmt.Fprintf(&b, "w := %sOf(&back); %s", e.Name, strings.Join(checks, "")) | |||
| 414 | + | // In place: dst aliases the Raw that d's borrowed fields view. | |||
| 415 | + | fmt.Fprintf(&b, "in := xturbo.Event{Raw: ev.Raw}; vi := %sOf(&in); di := %sEventData{%s}; di.Put(&in.Raw); if in.Raw != out { t.Fatal(%q) };", | |||
| 416 | + | e.Name, e.Name, strings.ReplaceAll(strings.Join(fields, ", "), v+".", "vi."), e.Name+" in-place Put") | |||
| 417 | + | } | |||
| 418 | + | b.WriteString("};\n") | |||
| 419 | + | } | |||
| 420 | + | b.WriteString("})\n}\n") | |||
| 421 | + | body := b.String() | |||
| 422 | + | imports := "import \"testing\"\nimport \"bnema.dev/xturbo\"" | |||
| 423 | + | if len(goldens) > 0 { | |||
| 424 | + | imports += "\nimport \"bnema.dev/xturbo/internal/xtest\"" | |||
| 425 | + | } | |||
| 426 | + | if strings.Contains(body, "bytes.") { | |||
| 427 | + | imports += "\nimport \"bytes\"" | |||
| 428 | + | } | |||
| 429 | + | data := struct { | |||
| 430 | + | Pkg string | |||
| 431 | + | Decls []string | |||
| 432 | + | }{Pkg: m.Pkg, Decls: []string{imports, body}} | |||
| 433 | + | return writeTemplate(filepath.Join(m.Pkg, "events_fuzz_test.go"), "events", data) | |||
| 434 | + | } | |||
A cmd/xtgen/events_test.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | package main | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "strings" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // TestEventLayouts pins wire offsets that the X11 protocol defines, checked | |
| 9 | + | // against the event containers (independent of generated code). | |
| 10 | + | func TestEventLayouts(t *testing.T) { | |
| 11 | + | want := map[string]map[string]int{ | |
| 12 | + | "xproto.MapRequest": {"Parent": 4, "Window": 8}, | |
| 13 | + | "xproto.ConfigureRequest": {"StackMode": 1, "Parent": 4, "Window": 8, "Sibling": 12, "X": 16, "Y": 18, "Width": 20, "Height": 22, "BorderWidth": 24, "ValueMask": 26}, | |
| 14 | + | "xproto.KeymapNotify": {"Keys": 1}, | |
| 15 | + | "xproto.ClientMessage": {"Format": 1, "Window": 4, "Type": 8, "Data": 12}, | |
| 16 | + | "xproto.PropertyNotify": {"Window": 4, "Atom": 8, "Time": 12, "State": 16}, | |
| 17 | + | "xfixes.SelectionNotify": {"Subtype": 1, "Window": 4, "Owner": 8, "Selection": 12, "Timestamp": 16, "SelectionTimestamp": 20}, | |
| 18 | + | "shape.Notify": {"ShapeKind": 1, "AffectedWindow": 4, "ServerTime": 16, "Shaped": 20}, | |
| 19 | + | } | |
| 20 | + | copies := 0 | |
| 21 | + | for _, m := range testModules(t) { | |
| 22 | + | events, err := resolveEvents(m) | |
| 23 | + | if err != nil { | |
| 24 | + | t.Fatal(err) | |
| 25 | + | } | |
| 26 | + | for _, e := range events { | |
| 27 | + | if e.Copy != nil { | |
| 28 | + | copies++ | |
| 29 | + | if e.C != e.Copy.C { | |
| 30 | + | t.Errorf("%s: copy does not share its source layout", e.Name) | |
| 31 | + | } | |
| 32 | + | continue | |
| 33 | + | } | |
| 34 | + | exp := want[m.Pkg+"."+e.Name] | |
| 35 | + | for _, mem := range e.C.Members { | |
| 36 | + | if off, ok := exp[mem.Method]; ok { | |
| 37 | + | if mem.Static != off { | |
| 38 | + | t.Errorf("%s.%s.%s at %d want %d", m.Pkg, e.Name, mem.Method, mem.Static, off) | |
| 39 | + | } | |
| 40 | + | delete(exp, mem.Method) | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | if len(exp) != 0 { | |
| 44 | + | t.Errorf("%s.%s: missing %v", m.Pkg, e.Name, exp) | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | if copies != 5 { | |
| 49 | + | t.Errorf("xproto has 5 eventcopy, got %d", copies) | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | func TestEventGeneratorRejects(t *testing.T) { | |
| 54 | + | for _, tc := range []struct{ body, want string }{ | |
| 55 | + | {`<event name="E" number="200" xge="true"><pad bytes="22"/></event>`, "xge evtype"}, | |
| 56 | + | {`<event name="E" number="1"><field type="CARD32" name="a"/></event>`, "header data byte"}, | |
| 57 | + | {`<eventcopy name="C" number="3" ref="Missing"/>`, "unknown event"}, | |
| 58 | + | {`<event name="E" number="2"><pad bytes="1"/><list type="CARD8" name="l"><fieldref>n</fieldref></list></event>`, "first 32 bytes"}, | |
| 59 | + | } { | |
| 60 | + | // Some constructs are already rejected by the parser; either stage must | |
| 61 | + | // fail with a source location. | |
| 62 | + | n, err := parseXML("ev.xml", strings.NewReader(`<xcb header="test" extension-name="Test" extension-xname="TEST">`+tc.body+`</xcb>`)) | |
| 63 | + | if err == nil { | |
| 64 | + | var ms []*Module | |
| 65 | + | if ms, err = resolveModules([]*Node{n}); err != nil { | |
| 66 | + | t.Fatal(err) | |
| 67 | + | } | |
| 68 | + | _, err = resolveEvents(ms[0]) | |
| 69 | + | } | |
| 70 | + | if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "ev.xml:1:") { | |
| 71 | + | t.Errorf("%s: got %v", tc.want, err) | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | func TestErrorConstNames(t *testing.T) { | |
| 77 | + | for in, want := range map[string]string{"Window": "BadWindow", "BadRegion": "BadRegion", "PictFormat": "BadPictFormat", "IDChoice": "BadIDChoice"} { | |
| 78 | + | if got := errorConst(in); got != want { | |
| 79 | + | t.Errorf("%s => %s want %s", in, got, want) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | } | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | package main | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "strings" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // TestEventLayouts pins wire offsets that the X11 protocol defines, checked | |||
| 9 | + | // against the event containers (independent of generated code). | |||
| 10 | + | func TestEventLayouts(t *testing.T) { | |||
| 11 | + | want := map[string]map[string]int{ | |||
| 12 | + | "xproto.MapRequest": {"Parent": 4, "Window": 8}, | |||
| 13 | + | "xproto.ConfigureRequest": {"StackMode": 1, "Parent": 4, "Window": 8, "Sibling": 12, "X": 16, "Y": 18, "Width": 20, "Height": 22, "BorderWidth": 24, "ValueMask": 26}, | |||
| 14 | + | "xproto.KeymapNotify": {"Keys": 1}, | |||
| 15 | + | "xproto.ClientMessage": {"Format": 1, "Window": 4, "Type": 8, "Data": 12}, | |||
| 16 | + | "xproto.PropertyNotify": {"Window": 4, "Atom": 8, "Time": 12, "State": 16}, | |||
| 17 | + | "xfixes.SelectionNotify": {"Subtype": 1, "Window": 4, "Owner": 8, "Selection": 12, "Timestamp": 16, "SelectionTimestamp": 20}, | |||
| 18 | + | "shape.Notify": {"ShapeKind": 1, "AffectedWindow": 4, "ServerTime": 16, "Shaped": 20}, | |||
| 19 | + | } | |||
| 20 | + | copies := 0 | |||
| 21 | + | for _, m := range testModules(t) { | |||
| 22 | + | events, err := resolveEvents(m) | |||
| 23 | + | if err != nil { | |||
| 24 | + | t.Fatal(err) | |||
| 25 | + | } | |||
| 26 | + | for _, e := range events { | |||
| 27 | + | if e.Copy != nil { | |||
| 28 | + | copies++ | |||
| 29 | + | if e.C != e.Copy.C { | |||
| 30 | + | t.Errorf("%s: copy does not share its source layout", e.Name) | |||
| 31 | + | } | |||
| 32 | + | continue | |||
| 33 | + | } | |||
| 34 | + | exp := want[m.Pkg+"."+e.Name] | |||
| 35 | + | for _, mem := range e.C.Members { | |||
| 36 | + | if off, ok := exp[mem.Method]; ok { | |||
| 37 | + | if mem.Static != off { | |||
| 38 | + | t.Errorf("%s.%s.%s at %d want %d", m.Pkg, e.Name, mem.Method, mem.Static, off) | |||
| 39 | + | } | |||
| 40 | + | delete(exp, mem.Method) | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | if len(exp) != 0 { | |||
| 44 | + | t.Errorf("%s.%s: missing %v", m.Pkg, e.Name, exp) | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | if copies != 5 { | |||
| 49 | + | t.Errorf("xproto has 5 eventcopy, got %d", copies) | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | func TestEventGeneratorRejects(t *testing.T) { | |||
| 54 | + | for _, tc := range []struct{ body, want string }{ | |||
| 55 | + | {`<event name="E" number="200" xge="true"><pad bytes="22"/></event>`, "xge evtype"}, | |||
| 56 | + | {`<event name="E" number="1"><field type="CARD32" name="a"/></event>`, "header data byte"}, | |||
| 57 | + | {`<eventcopy name="C" number="3" ref="Missing"/>`, "unknown event"}, | |||
| 58 | + | {`<event name="E" number="2"><pad bytes="1"/><list type="CARD8" name="l"><fieldref>n</fieldref></list></event>`, "first 32 bytes"}, | |||
| 59 | + | } { | |||
| 60 | + | // Some constructs are already rejected by the parser; either stage must | |||
| 61 | + | // fail with a source location. | |||
| 62 | + | n, err := parseXML("ev.xml", strings.NewReader(`<xcb header="test" extension-name="Test" extension-xname="TEST">`+tc.body+`</xcb>`)) | |||
| 63 | + | if err == nil { | |||
| 64 | + | var ms []*Module | |||
| 65 | + | if ms, err = resolveModules([]*Node{n}); err != nil { | |||
| 66 | + | t.Fatal(err) | |||
| 67 | + | } | |||
| 68 | + | _, err = resolveEvents(ms[0]) | |||
| 69 | + | } | |||
| 70 | + | if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "ev.xml:1:") { | |||
| 71 | + | t.Errorf("%s: got %v", tc.want, err) | |||
| 72 | + | } | |||
| 73 | + | } | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | func TestErrorConstNames(t *testing.T) { | |||
| 77 | + | for in, want := range map[string]string{"Window": "BadWindow", "BadRegion": "BadRegion", "PictFormat": "BadPictFormat", "IDChoice": "BadIDChoice"} { | |||
| 78 | + | if got := errorConst(in); got != want { | |||
| 79 | + | t.Errorf("%s => %s want %s", in, got, want) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | } | |||
M cmd/xtgen/ir.go +1 -0
| @@ -36,4 +36,5 @@ type Module struct { | |||
| 36 | 36 | Symbols map[string]*Type | |
| 37 | 37 | Names map[string]*Node | |
| 38 | 38 | Decls, SizeChecks []string | |
| 39 | + | NumEvents, NumErrors int // extension event/error code ranges | |
| 39 | 40 | } | |
| @@ -36,4 +36,5 @@ type Module struct { | |||||
| 36 | Symbols map[string]*Type | 36 | Symbols map[string]*Type | ||
| 37 | Names map[string]*Node | 37 | Names map[string]*Node | ||
| 38 | Decls, SizeChecks []string | 38 | Decls, SizeChecks []string | ||
| 39 | + | NumEvents, NumErrors int // extension event/error code ranges | |||
| 39 | } | 40 | } | ||
M cmd/xtgen/main.go +3 -0
| @@ -35,6 +35,9 @@ func main() { | |||
| 35 | 35 | if err == nil { | |
| 36 | 36 | err = emitReplies(modules) | |
| 37 | 37 | } | |
| 38 | + | if err == nil { | |
| 39 | + | err = emitEvents(modules) | |
| 40 | + | } | |
| 38 | 41 | if err != nil { | |
| 39 | 42 | fmt.Fprintln(os.Stderr, err) | |
| 40 | 43 | os.Exit(1) | |
| @@ -35,6 +35,9 @@ func main() { | |||||
| 35 | if err == nil { | 35 | if err == nil { | ||
| 36 | err = emitReplies(modules) | 36 | err = emitReplies(modules) | ||
| 37 | } | 37 | } | ||
| 38 | + | if err == nil { | |||
| 39 | + | err = emitEvents(modules) | |||
| 40 | + | } | |||
| 38 | if err != nil { | 41 | if err != nil { | ||
| 39 | fmt.Fprintln(os.Stderr, err) | 42 | fmt.Fprintln(os.Stderr, err) | ||
| 40 | os.Exit(1) | 43 | os.Exit(1) | ||
M cmd/xtgen/replies.go +39 -10
| @@ -58,14 +58,43 @@ func elemName(goType string) string { | |||
| 58 | 58 | return strings.ToUpper(goType[:1]) + goType[1:] | |
| 59 | 59 | } | |
| 60 | 60 | ||
| 61 | - | // buildContainer lowers XML members. Replies start at byte 1 (the first XML | |
| 62 | - | // member occupies the header data byte) and continue at byte 8. | |
| 63 | - | func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) { | |
| 61 | + | // layout selects where a container's XML members land on the wire. | |
| 62 | + | type layout int | |
| 63 | + | ||
| 64 | + | const ( | |
| 65 | + | layoutStruct layout = iota // members from byte 0 | |
| 66 | + | layoutReply // byte 1, then byte 8 (after seq and length) | |
| 67 | + | layoutEvent // byte 1, then byte 4 (after seq) | |
| 68 | + | layoutEventNoSeq // contiguous from byte 1 | |
| 69 | + | layoutXGE // from byte 10 (after extension, seq, length, evtype) | |
| 70 | + | ) | |
| 71 | + | ||
| 72 | + | // headerJump returns the offset following the header data byte, or 0 when the | |
| 73 | + | // layout has no data byte. | |
| 74 | + | func (l layout) headerJump() int { | |
| 75 | + | switch l { | |
| 76 | + | case layoutReply: | |
| 77 | + | return 8 | |
| 78 | + | case layoutEvent: | |
| 79 | + | return 4 | |
| 80 | + | } | |
| 81 | + | return 0 | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // buildContainer lowers XML members. Replies and events start at byte 1 (the | |
| 85 | + | // first XML member occupies the header data byte) and continue after the | |
| 86 | + | // sequence (and, for replies, length) header. | |
| 87 | + | func buildContainer(m *Module, name, recv string, nodes []*Node, lay layout, reserved string) (*container, error) { | |
| 88 | + | reply := lay == layoutReply | |
| 64 | 89 | c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m} | |
| 65 | 90 | off := 0 | |
| 66 | - | if reply { | |
| 91 | + | switch lay { | |
| 92 | + | case layoutReply, layoutEvent, layoutEventNoSeq: | |
| 67 | 93 | off = 1 | |
| 94 | + | case layoutXGE: | |
| 95 | + | off = 10 | |
| 68 | 96 | } | |
| 97 | + | what := map[layout]string{layoutStruct: "struct", layoutReply: "reply", layoutEvent: "event", layoutEventNoSeq: "event", layoutXGE: "event"}[lay] | |
| 69 | 98 | first := true | |
| 70 | 99 | methods := map[string]bool{} | |
| 71 | 100 | for _, n := range nodes { | |
| @@ -138,13 +167,13 @@ func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, res | |||
| 138 | 167 | c.Dynamic = len(c.Members) | |
| 139 | 168 | } | |
| 140 | 169 | default: | |
| 141 | - | return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind) | |
| 170 | + | return nil, n.fail("unsupported %s member %s", what, n.Kind) | |
| 142 | 171 | } | |
| 143 | - | if reply && first { | |
| 172 | + | if jump := lay.headerJump(); jump > 0 && first { | |
| 144 | 173 | if mem.Size != 1 { | |
| 145 | - | return nil, n.fail("first reply member must occupy the header data byte") | |
| 174 | + | return nil, n.fail("first %s member must occupy the header data byte", what) | |
| 146 | 175 | } | |
| 147 | - | off = 8 | |
| 176 | + | off = jump | |
| 148 | 177 | first = false | |
| 149 | 178 | c.Members = append(c.Members, mem) | |
| 150 | 179 | continue | |
| @@ -340,7 +369,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) { | |||
| 340 | 369 | return nil, nil, err | |
| 341 | 370 | } | |
| 342 | 371 | } | |
| 343 | - | c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw") | |
| 372 | + | c, err := buildContainer(m, name+"Reply", "r", r.Children, layoutReply, "Raw") | |
| 344 | 373 | if err != nil { | |
| 345 | 374 | return nil, nil, err | |
| 346 | 375 | } | |
| @@ -357,7 +386,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) { | |||
| 357 | 386 | return nil, nil, err | |
| 358 | 387 | } | |
| 359 | 388 | } | |
| 360 | - | c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size") | |
| 389 | + | c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, layoutStruct, "Raw Size") | |
| 361 | 390 | if err != nil { | |
| 362 | 391 | return nil, nil, err | |
| 363 | 392 | } | |
| @@ -58,14 +58,43 @@ func elemName(goType string) string { | |||||
| 58 | return strings.ToUpper(goType[:1]) + goType[1:] | 58 | return strings.ToUpper(goType[:1]) + goType[1:] | ||
| 59 | } | 59 | } | ||
| 60 | 60 | ||||
| 61 | - | // buildContainer lowers XML members. Replies start at byte 1 (the first XML | 61 | + | // layout selects where a container's XML members land on the wire. |
| 62 | - | // member occupies the header data byte) and continue at byte 8. | 62 | + | type layout int |
| 63 | - | func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, reserved string) (*container, error) { | 63 | + | |
| 64 | + | const ( | |||
| 65 | + | layoutStruct layout = iota // members from byte 0 | |||
| 66 | + | layoutReply // byte 1, then byte 8 (after seq and length) | |||
| 67 | + | layoutEvent // byte 1, then byte 4 (after seq) | |||
| 68 | + | layoutEventNoSeq // contiguous from byte 1 | |||
| 69 | + | layoutXGE // from byte 10 (after extension, seq, length, evtype) | |||
| 70 | + | ) | |||
| 71 | + | ||||
| 72 | + | // headerJump returns the offset following the header data byte, or 0 when the | |||
| 73 | + | // layout has no data byte. | |||
| 74 | + | func (l layout) headerJump() int { | |||
| 75 | + | switch l { | |||
| 76 | + | case layoutReply: | |||
| 77 | + | return 8 | |||
| 78 | + | case layoutEvent: | |||
| 79 | + | return 4 | |||
| 80 | + | } | |||
| 81 | + | return 0 | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // buildContainer lowers XML members. Replies and events start at byte 1 (the | |||
| 85 | + | // first XML member occupies the header data byte) and continue after the | |||
| 86 | + | // sequence (and, for replies, length) header. | |||
| 87 | + | func buildContainer(m *Module, name, recv string, nodes []*Node, lay layout, reserved string) (*container, error) { | |||
| 88 | + | reply := lay == layoutReply | |||
| 64 | c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m} | 89 | c := &container{Name: name, Recv: recv, Reply: reply, Dynamic: -1, Module: m} | ||
| 65 | off := 0 | 90 | off := 0 | ||
| 66 | - | if reply { | 91 | + | switch lay { |
| 92 | + | case layoutReply, layoutEvent, layoutEventNoSeq: | |||
| 67 | off = 1 | 93 | off = 1 | ||
| 94 | + | case layoutXGE: | |||
| 95 | + | off = 10 | |||
| 68 | } | 96 | } | ||
| 97 | + | what := map[layout]string{layoutStruct: "struct", layoutReply: "reply", layoutEvent: "event", layoutEventNoSeq: "event", layoutXGE: "event"}[lay] | |||
| 69 | first := true | 98 | first := true | ||
| 70 | methods := map[string]bool{} | 99 | methods := map[string]bool{} | ||
| 71 | for _, n := range nodes { | 100 | for _, n := range nodes { | ||
| @@ -138,13 +167,13 @@ func buildContainer(m *Module, name, recv string, nodes []*Node, reply bool, res | |||||
| 138 | c.Dynamic = len(c.Members) | 167 | c.Dynamic = len(c.Members) | ||
| 139 | } | 168 | } | ||
| 140 | default: | 169 | default: | ||
| 141 | - | return nil, n.fail("unsupported %s member %s", map[bool]string{true: "reply", false: "struct"}[reply], n.Kind) | 170 | + | return nil, n.fail("unsupported %s member %s", what, n.Kind) |
| 142 | } | 171 | } | ||
| 143 | - | if reply && first { | 172 | + | if jump := lay.headerJump(); jump > 0 && first { |
| 144 | if mem.Size != 1 { | 173 | if mem.Size != 1 { | ||
| 145 | - | return nil, n.fail("first reply member must occupy the header data byte") | 174 | + | return nil, n.fail("first %s member must occupy the header data byte", what) |
| 146 | } | 175 | } | ||
| 147 | - | off = 8 | 176 | + | off = jump |
| 148 | first = false | 177 | first = false | ||
| 149 | c.Members = append(c.Members, mem) | 178 | c.Members = append(c.Members, mem) | ||
| 150 | continue | 179 | continue | ||
| @@ -340,7 +369,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) { | |||||
| 340 | return nil, nil, err | 369 | return nil, nil, err | ||
| 341 | } | 370 | } | ||
| 342 | } | 371 | } | ||
| 343 | - | c, err := buildContainer(m, name+"Reply", "r", r.Children, true, "Raw") | 372 | + | c, err := buildContainer(m, name+"Reply", "r", r.Children, layoutReply, "Raw") |
| 344 | if err != nil { | 373 | if err != nil { | ||
| 345 | return nil, nil, err | 374 | return nil, nil, err | ||
| 346 | } | 375 | } | ||
| @@ -357,7 +386,7 @@ func resolveReplies(m *Module) ([]replyIR, []*container, error) { | |||||
| 357 | return nil, nil, err | 386 | return nil, nil, err | ||
| 358 | } | 387 | } | ||
| 359 | } | 388 | } | ||
| 360 | - | c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, false, "Raw Size") | 389 | + | c, err := buildContainer(m, t.GoType+"View", "v", t.Node.Children, layoutStruct, "Raw Size") |
| 361 | if err != nil { | 390 | if err != nil { | ||
| 362 | return nil, nil, err | 391 | return nil, nil, err | ||
| 363 | } | 392 | } | ||
M cmd/xtgen/replies_test.go +1 -1
| @@ -60,7 +60,7 @@ func TestReplyReservedGetter(t *testing.T) { | |||
| 60 | 60 | {Kind: "pad", Attr: map[string]string{"bytes": "1"}}, | |
| 61 | 61 | {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}}, | |
| 62 | 62 | } | |
| 63 | - | c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw") | |
| 63 | + | c, err := buildContainer(m, "XReply", "r", nodes, layoutReply, "Raw") | |
| 64 | 64 | if err != nil { | |
| 65 | 65 | t.Fatal(err) | |
| 66 | 66 | } | |
| @@ -60,7 +60,7 @@ func TestReplyReservedGetter(t *testing.T) { | |||||
| 60 | {Kind: "pad", Attr: map[string]string{"bytes": "1"}}, | 60 | {Kind: "pad", Attr: map[string]string{"bytes": "1"}}, | ||
| 61 | {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}}, | 61 | {Kind: "field", Attr: map[string]string{"name": "raw", "type": "CARD32"}}, | ||
| 62 | } | 62 | } | ||
| 63 | - | c, err := buildContainer(m, "XReply", "r", nodes, true, "Raw") | 63 | + | c, err := buildContainer(m, "XReply", "r", nodes, layoutReply, "Raw") |
| 64 | if err != nil { | 64 | if err != nil { | ||
| 65 | t.Fatal(err) | 65 | t.Fatal(err) | ||
| 66 | } | 66 | } | ||
A cmd/xtgen/templates/errors.tmpl +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |
| 2 | + | package {{.Pkg}} | |
| 3 | + | {{range .Decls}} | |
| 4 | + | {{.}} | |
| 5 | + | {{end}} | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |||
| 2 | + | package {{.Pkg}} | |||
| 3 | + | {{range .Decls}} | |||
| 4 | + | {{.}} | |||
| 5 | + | {{end}} | |||
A cmd/xtgen/templates/events.tmpl +5 -0
| @@ -0,0 +1,5 @@ | |||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |
| 2 | + | package {{.Pkg}} | |
| 3 | + | {{range .Decls}} | |
| 4 | + | {{.}} | |
| 5 | + | {{end}} | |
| @@ -0,0 +1,5 @@ | |||||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |||
| 2 | + | package {{.Pkg}} | |||
| 3 | + | {{range .Decls}} | |||
| 4 | + | {{.}} | |||
| 5 | + | {{end}} | |||
M cmd/xtgen/templates/ext.tmpl +4 -0
| @@ -8,3 +8,7 @@ const ( | |||
| 8 | 8 | {{end}} extCount | |
| 9 | 9 | ) | |
| 10 | 10 | var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}} | |
| 11 | + | // extEvents and extErrors are the event and error code ranges each extension | |
| 12 | + | // declares in the pinned XML; ExtTable.Set uses them to fill reverse maps. | |
| 13 | + | var extEvents = [extCount]uint8{0, {{range .}}{{.NumEvents}},{{end}}} | |
| 14 | + | var extErrors = [extCount]uint8{0, {{range .}}{{.NumErrors}},{{end}}} | |
| @@ -8,3 +8,7 @@ const ( | |||||
| 8 | {{end}} extCount | 8 | {{end}} extCount | ||
| 9 | ) | 9 | ) | ||
| 10 | var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}} | 10 | var ExtXName = [extCount]string{"", {{range .}}{{printf "%q" .XName}},{{end}}} | ||
| 11 | + | // extEvents and extErrors are the event and error code ranges each extension | |||
| 12 | + | // declares in the pinned XML; ExtTable.Set uses them to fill reverse maps. | |||
| 13 | + | var extEvents = [extCount]uint8{0, {{range .}}{{.NumEvents}},{{end}}} | |||
| 14 | + | var extErrors = [extCount]uint8{0, {{range .}}{{.NumErrors}},{{end}}} | |||
A event.go +77 -0
| @@ -0,0 +1,77 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import "encoding/binary" | |
| 4 | + | ||
| 5 | + | // EventKind identifies a decoded event: uint16(ExtID)<<8 | code. Core events | |
| 6 | + | // use the wire code (2..34); extension events the code relative to the | |
| 7 | + | // extension's first event; XGE events 0x80|evtype. | |
| 8 | + | type EventKind uint16 | |
| 9 | + | ||
| 10 | + | // KindUnknown marks an event whose code or XGE extension is not known. | |
| 11 | + | const KindUnknown EventKind = 0xFFFF | |
| 12 | + | ||
| 13 | + | const ( | |
| 14 | + | codeGenericEvent = 35 | |
| 15 | + | codeKeymapNotify = 11 | |
| 16 | + | ) | |
| 17 | + | ||
| 18 | + | // Event is a decoded event value. Raw is a copy and survives Dispatch; Tail | |
| 19 | + | // borrows the read buffer (XGE bytes beyond 32) and is valid only until the | |
| 20 | + | // Handler callback returns. Copy what you keep. | |
| 21 | + | type Event struct { | |
| 22 | + | Kind EventKind | |
| 23 | + | SendEvent bool | |
| 24 | + | Ext ExtID | |
| 25 | + | Seq Seq // wire sequence; Conn widens it. 0 for KeymapNotify. | |
| 26 | + | Raw [32]byte | |
| 27 | + | Tail []byte | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // DecodeEvent decodes one event packet at the start of raw. n is the number | |
| 31 | + | // of bytes consumed (32, or 32+4*length for XGE). ok is false for error and | |
| 32 | + | // reply packets and for short input. Unknown codes decode with | |
| 33 | + | // Kind == KindUnknown and ok == true. A nil ext decodes core events only. | |
| 34 | + | // The returned Seq is the 16-bit wire value; Conn widens it. | |
| 35 | + | func DecodeEvent(raw []byte, ext *ExtTable) (ev Event, n int, ok bool) { | |
| 36 | + | if len(raw) < 32 { | |
| 37 | + | return Event{}, 0, false | |
| 38 | + | } | |
| 39 | + | code := raw[0] & 0x7f | |
| 40 | + | if code < 2 { | |
| 41 | + | return Event{}, 0, false | |
| 42 | + | } | |
| 43 | + | n = 32 | |
| 44 | + | if code == codeGenericEvent { | |
| 45 | + | length := uint64(binary.LittleEndian.Uint32(raw[4:])) | |
| 46 | + | if uint64(len(raw)-32)/4 < length { | |
| 47 | + | return Event{}, 0, false | |
| 48 | + | } | |
| 49 | + | n = 32 + int(length)*4 | |
| 50 | + | ev.Tail = raw[32:n:n] | |
| 51 | + | } | |
| 52 | + | copy(ev.Raw[:], raw[:32]) | |
| 53 | + | ev.SendEvent = raw[0]&0x80 != 0 | |
| 54 | + | if code != codeKeymapNotify { | |
| 55 | + | ev.Seq = Seq(binary.LittleEndian.Uint16(raw[2:])) | |
| 56 | + | } | |
| 57 | + | ev.Kind = KindUnknown | |
| 58 | + | switch { | |
| 59 | + | case code < codeGenericEvent: | |
| 60 | + | ev.Kind = EventKind(code) | |
| 61 | + | case code == codeGenericEvent: | |
| 62 | + | if ext == nil { | |
| 63 | + | break | |
| 64 | + | } | |
| 65 | + | evtype := binary.LittleEndian.Uint16(raw[8:]) | |
| 66 | + | if id, found := ext.ByMajor(raw[1]); found && evtype < 0x80 { | |
| 67 | + | ev.Ext = id | |
| 68 | + | ev.Kind = EventKind(id)<<8 | 0x80 | EventKind(evtype) | |
| 69 | + | } | |
| 70 | + | case code >= 64 && ext != nil: | |
| 71 | + | if id, rel, found := ext.ByEvent(code); found { | |
| 72 | + | ev.Ext = id | |
| 73 | + | ev.Kind = EventKind(id)<<8 | EventKind(rel) | |
| 74 | + | } | |
| 75 | + | } | |
| 76 | + | return ev, n, true | |
| 77 | + | } | |
| @@ -0,0 +1,77 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import "encoding/binary" | |||
| 4 | + | ||||
| 5 | + | // EventKind identifies a decoded event: uint16(ExtID)<<8 | code. Core events | |||
| 6 | + | // use the wire code (2..34); extension events the code relative to the | |||
| 7 | + | // extension's first event; XGE events 0x80|evtype. | |||
| 8 | + | type EventKind uint16 | |||
| 9 | + | ||||
| 10 | + | // KindUnknown marks an event whose code or XGE extension is not known. | |||
| 11 | + | const KindUnknown EventKind = 0xFFFF | |||
| 12 | + | ||||
| 13 | + | const ( | |||
| 14 | + | codeGenericEvent = 35 | |||
| 15 | + | codeKeymapNotify = 11 | |||
| 16 | + | ) | |||
| 17 | + | ||||
| 18 | + | // Event is a decoded event value. Raw is a copy and survives Dispatch; Tail | |||
| 19 | + | // borrows the read buffer (XGE bytes beyond 32) and is valid only until the | |||
| 20 | + | // Handler callback returns. Copy what you keep. | |||
| 21 | + | type Event struct { | |||
| 22 | + | Kind EventKind | |||
| 23 | + | SendEvent bool | |||
| 24 | + | Ext ExtID | |||
| 25 | + | Seq Seq // wire sequence; Conn widens it. 0 for KeymapNotify. | |||
| 26 | + | Raw [32]byte | |||
| 27 | + | Tail []byte | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // DecodeEvent decodes one event packet at the start of raw. n is the number | |||
| 31 | + | // of bytes consumed (32, or 32+4*length for XGE). ok is false for error and | |||
| 32 | + | // reply packets and for short input. Unknown codes decode with | |||
| 33 | + | // Kind == KindUnknown and ok == true. A nil ext decodes core events only. | |||
| 34 | + | // The returned Seq is the 16-bit wire value; Conn widens it. | |||
| 35 | + | func DecodeEvent(raw []byte, ext *ExtTable) (ev Event, n int, ok bool) { | |||
| 36 | + | if len(raw) < 32 { | |||
| 37 | + | return Event{}, 0, false | |||
| 38 | + | } | |||
| 39 | + | code := raw[0] & 0x7f | |||
| 40 | + | if code < 2 { | |||
| 41 | + | return Event{}, 0, false | |||
| 42 | + | } | |||
| 43 | + | n = 32 | |||
| 44 | + | if code == codeGenericEvent { | |||
| 45 | + | length := uint64(binary.LittleEndian.Uint32(raw[4:])) | |||
| 46 | + | if uint64(len(raw)-32)/4 < length { | |||
| 47 | + | return Event{}, 0, false | |||
| 48 | + | } | |||
| 49 | + | n = 32 + int(length)*4 | |||
| 50 | + | ev.Tail = raw[32:n:n] | |||
| 51 | + | } | |||
| 52 | + | copy(ev.Raw[:], raw[:32]) | |||
| 53 | + | ev.SendEvent = raw[0]&0x80 != 0 | |||
| 54 | + | if code != codeKeymapNotify { | |||
| 55 | + | ev.Seq = Seq(binary.LittleEndian.Uint16(raw[2:])) | |||
| 56 | + | } | |||
| 57 | + | ev.Kind = KindUnknown | |||
| 58 | + | switch { | |||
| 59 | + | case code < codeGenericEvent: | |||
| 60 | + | ev.Kind = EventKind(code) | |||
| 61 | + | case code == codeGenericEvent: | |||
| 62 | + | if ext == nil { | |||
| 63 | + | break | |||
| 64 | + | } | |||
| 65 | + | evtype := binary.LittleEndian.Uint16(raw[8:]) | |||
| 66 | + | if id, found := ext.ByMajor(raw[1]); found && evtype < 0x80 { | |||
| 67 | + | ev.Ext = id | |||
| 68 | + | ev.Kind = EventKind(id)<<8 | 0x80 | EventKind(evtype) | |||
| 69 | + | } | |||
| 70 | + | case code >= 64 && ext != nil: | |||
| 71 | + | if id, rel, found := ext.ByEvent(code); found { | |||
| 72 | + | ev.Ext = id | |||
| 73 | + | ev.Kind = EventKind(id)<<8 | EventKind(rel) | |||
| 74 | + | } | |||
| 75 | + | } | |||
| 76 | + | return ev, n, true | |||
| 77 | + | } | |||
A events_test.go +499 -0
| @@ -0,0 +1,499 @@ | |||
| 1 | + | package xturbo_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "encoding/binary" | |
| 6 | + | "testing" | |
| 7 | + | ||
| 8 | + | "bnema.dev/xturbo" | |
| 9 | + | "bnema.dev/xturbo/internal/xtest" | |
| 10 | + | "bnema.dev/xturbo/render" | |
| 11 | + | "bnema.dev/xturbo/shape" | |
| 12 | + | "bnema.dev/xturbo/xfixes" | |
| 13 | + | "bnema.dev/xturbo/xproto" | |
| 14 | + | ) | |
| 15 | + | ||
| 16 | + | // Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the | |
| 17 | + | // goldens were captured; the BadRegion and SelectionNotify goldens confirm it. | |
| 18 | + | const ( | |
| 19 | + | xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140 | |
| 20 | + | geMajor = 0x83 // hand-written XGE golden | |
| 21 | + | ) | |
| 22 | + | ||
| 23 | + | func loadEvent(t testing.TB, name string) []byte { | |
| 24 | + | t.Helper() | |
| 25 | + | raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex") | |
| 26 | + | if err != nil { | |
| 27 | + | t.Fatal(err) | |
| 28 | + | } | |
| 29 | + | return raw | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func goldenExt() *xturbo.ExtTable { | |
| 33 | + | var ext xturbo.ExtTable | |
| 34 | + | ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError) | |
| 35 | + | ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0) | |
| 36 | + | ext.Set(xturbo.ExtShape, 129, 64, 0) | |
| 37 | + | return &ext | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event { | |
| 41 | + | t.Helper() | |
| 42 | + | ev, n, ok := xturbo.DecodeEvent(raw, ext) | |
| 43 | + | if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) { | |
| 44 | + | t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok) | |
| 45 | + | } | |
| 46 | + | return ev | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | // Expected values follow from the capture script (second client's window at | |
| 50 | + | // 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage | |
| 51 | + | // with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden | |
| 52 | + | // offsets defined by the X11 protocol, not from generated code. | |
| 53 | + | func TestEventGoldens(t *testing.T) { | |
| 54 | + | u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) } | |
| 55 | + | ext := goldenExt() | |
| 56 | + | core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) { | |
| 57 | + | raw := loadEvent(t, "xproto/"+name) | |
| 58 | + | ev := decode(t, raw, ext) | |
| 59 | + | if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil { | |
| 60 | + | t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq) | |
| 61 | + | } | |
| 62 | + | return raw, ev | |
| 63 | + | } | |
| 64 | + | t.Run("MapRequest", func(t *testing.T) { | |
| 65 | + | raw, ev := core("MapRequest", xproto.KindMapRequest) | |
| 66 | + | e := xproto.MapRequestOf(&ev) | |
| 67 | + | if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 { | |
| 68 | + | t.Fatal("fields") | |
| 69 | + | } | |
| 70 | + | d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()} | |
| 71 | + | var out [32]byte | |
| 72 | + | d.Put(&out) | |
| 73 | + | want := [32]byte(raw) | |
| 74 | + | want[2], want[3] = 0, 0 // Put leaves the sequence to the server | |
| 75 | + | if out != want { | |
| 76 | + | t.Fatalf("Put %x want %x", out, want) | |
| 77 | + | } | |
| 78 | + | }) | |
| 79 | + | t.Run("ConfigureRequest", func(t *testing.T) { | |
| 80 | + | raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest) | |
| 81 | + | e := xproto.ConfigureRequestOf(&ev) | |
| 82 | + | if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 || | |
| 83 | + | e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 || | |
| 84 | + | e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth { | |
| 85 | + | t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask()) | |
| 86 | + | } | |
| 87 | + | d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(), | |
| 88 | + | X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()} | |
| 89 | + | var out [32]byte | |
| 90 | + | d.Put(&out) | |
| 91 | + | if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] { | |
| 92 | + | t.Fatalf("Put %x", out) | |
| 93 | + | } | |
| 94 | + | }) | |
| 95 | + | t.Run("PropertyNotify", func(t *testing.T) { | |
| 96 | + | raw, ev := core("PropertyNotify", xproto.KindPropertyNotify) | |
| 97 | + | e := xproto.PropertyNotifyOf(&ev) | |
| 98 | + | if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 || | |
| 99 | + | e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue { | |
| 100 | + | t.Fatal("fields") | |
| 101 | + | } | |
| 102 | + | }) | |
| 103 | + | t.Run("ClientMessage", func(t *testing.T) { | |
| 104 | + | raw, ev := core("ClientMessage", xproto.KindClientMessage) | |
| 105 | + | e := xproto.ClientMessageOf(&ev) | |
| 106 | + | if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) { | |
| 107 | + | t.Fatal("fields") | |
| 108 | + | } | |
| 109 | + | words := e.Data().Data32() | |
| 110 | + | for i, w := range words { | |
| 111 | + | if w != uint32(0x01020304*(i+1)) { | |
| 112 | + | t.Fatalf("data32[%d] = %x", i, w) | |
| 113 | + | } | |
| 114 | + | } | |
| 115 | + | d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()} | |
| 116 | + | var out [32]byte | |
| 117 | + | d.Put(&out) | |
| 118 | + | want := [32]byte(raw) | |
| 119 | + | want[0] &^= 0x80 | |
| 120 | + | want[2], want[3] = 0, 0 | |
| 121 | + | if out != want { | |
| 122 | + | t.Fatalf("Put %x want %x", out, want) | |
| 123 | + | } | |
| 124 | + | }) | |
| 125 | + | t.Run("CreateNotify", func(t *testing.T) { | |
| 126 | + | _, ev := core("CreateNotify", xproto.KindCreateNotify) | |
| 127 | + | e := xproto.CreateNotifyOf(&ev) | |
| 128 | + | if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() { | |
| 129 | + | t.Fatal("fields") | |
| 130 | + | } | |
| 131 | + | }) | |
| 132 | + | t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) { | |
| 133 | + | _, ev := core("MapNotify", xproto.KindMapNotify) | |
| 134 | + | if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() { | |
| 135 | + | t.Fatal("MapNotify") | |
| 136 | + | } | |
| 137 | + | _, ev = core("UnmapNotify", xproto.KindUnmapNotify) | |
| 138 | + | if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() { | |
| 139 | + | t.Fatal("UnmapNotify") | |
| 140 | + | } | |
| 141 | + | _, ev = core("DestroyNotify", xproto.KindDestroyNotify) | |
| 142 | + | if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 { | |
| 143 | + | t.Fatal("DestroyNotify") | |
| 144 | + | } | |
| 145 | + | }) | |
| 146 | + | t.Run("SelectionNotify", func(t *testing.T) { | |
| 147 | + | _, ev := core("SelectionNotify", xproto.KindSelectionNotify) | |
| 148 | + | e := xproto.SelectionNotifyOf(&ev) | |
| 149 | + | if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone { | |
| 150 | + | t.Fatal("fields") | |
| 151 | + | } | |
| 152 | + | }) | |
| 153 | + | t.Run("xfixes/SelectionNotify", func(t *testing.T) { | |
| 154 | + | raw := loadEvent(t, "xfixes/SelectionNotify") | |
| 155 | + | ev := decode(t, raw, ext) | |
| 156 | + | if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 { | |
| 157 | + | t.Fatal("KindSelectionNotify value") | |
| 158 | + | } | |
| 159 | + | if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes { | |
| 160 | + | t.Fatalf("kind %x", ev.Kind) | |
| 161 | + | } | |
| 162 | + | e := xfixes.SelectionNotifyOf(&ev) | |
| 163 | + | if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) || | |
| 164 | + | e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() { | |
| 165 | + | t.Fatal("fields") | |
| 166 | + | } | |
| 167 | + | // Without XFIXES negotiated the same bytes are an unknown event. | |
| 168 | + | if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown { | |
| 169 | + | t.Fatal("nil ext must decode as unknown") | |
| 170 | + | } | |
| 171 | + | }) | |
| 172 | + | t.Run("KeymapNotify", func(t *testing.T) { | |
| 173 | + | raw := loadEvent(t, "xproto/KeymapNotify") | |
| 174 | + | ev := decode(t, raw, ext) | |
| 175 | + | if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 { | |
| 176 | + | t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq) | |
| 177 | + | } | |
| 178 | + | e := xproto.KeymapNotifyOf(&ev) | |
| 179 | + | if !bytes.Equal(e.Keys(), raw[1:]) { | |
| 180 | + | t.Fatal("keys") | |
| 181 | + | } | |
| 182 | + | d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])} | |
| 183 | + | var out [32]byte | |
| 184 | + | d.Put(&out) | |
| 185 | + | if !bytes.Equal(out[:], raw) { | |
| 186 | + | t.Fatalf("Put %x", out) | |
| 187 | + | } | |
| 188 | + | }) | |
| 189 | + | t.Run("GeGeneric", func(t *testing.T) { | |
| 190 | + | raw := loadEvent(t, "xproto/GeGeneric") | |
| 191 | + | ev := decode(t, raw, ext) | |
| 192 | + | if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 || | |
| 193 | + | !bytes.Equal(ev.Tail, raw[32:]) { | |
| 194 | + | t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail) | |
| 195 | + | } | |
| 196 | + | g, ok := xproto.GeGenericOf(&ev) | |
| 197 | + | if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) { | |
| 198 | + | t.Fatal("view") | |
| 199 | + | } | |
| 200 | + | // Unknown major: still framed so Dispatch skips the tail. | |
| 201 | + | ev, n, ok := xturbo.DecodeEvent(raw, nil) | |
| 202 | + | if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 { | |
| 203 | + | t.Fatal("unknown XGE framing") | |
| 204 | + | } | |
| 205 | + | ev.Tail = ev.Tail[:4] | |
| 206 | + | if _, ok := xproto.GeGenericOf(&ev); ok { | |
| 207 | + | t.Fatal("short Tail accepted") | |
| 208 | + | } | |
| 209 | + | }) | |
| 210 | + | } | |
| 211 | + | ||
| 212 | + | // TestEventDataPutInPlace re-encodes events into the Event.Raw their fields | |
| 213 | + | // were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's | |
| 214 | + | // Keys is a slice view over Raw. Put must read every field before writing dst. | |
| 215 | + | func TestEventDataPutInPlace(t *testing.T) { | |
| 216 | + | raw := loadEvent(t, "xproto/ClientMessage") | |
| 217 | + | ev := decode(t, raw, nil) | |
| 218 | + | e := xproto.ClientMessageOf(&ev) | |
| 219 | + | d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()} | |
| 220 | + | d.Put(&ev.Raw) | |
| 221 | + | want := [32]byte(raw) | |
| 222 | + | want[0] &^= 0x80 | |
| 223 | + | want[2], want[3] = 0, 0 | |
| 224 | + | if ev.Raw != want { | |
| 225 | + | t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want) | |
| 226 | + | } | |
| 227 | + | if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 { | |
| 228 | + | t.Fatalf("in-place Put allocates %v times", n) | |
| 229 | + | } | |
| 230 | + | raw = loadEvent(t, "xproto/KeymapNotify") | |
| 231 | + | ev = decode(t, raw, nil) | |
| 232 | + | keys := xproto.KeymapNotifyOf(&ev).Keys() | |
| 233 | + | k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)} | |
| 234 | + | k.Put(&ev.Raw) | |
| 235 | + | if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) { | |
| 236 | + | t.Fatalf("KeymapNotify in place %x", ev.Raw) | |
| 237 | + | } | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | func TestEventCopyAliases(t *testing.T) { | |
| 241 | + | if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 { | |
| 242 | + | t.Fatal("eventcopy kinds") | |
| 243 | + | } | |
| 244 | + | var raw [32]byte | |
| 245 | + | raw[0], raw[1] = 5, 3 // ButtonRelease, button 3 | |
| 246 | + | binary.LittleEndian.PutUint16(raw[24:], 0x100) | |
| 247 | + | ev := decode(t, raw[:], nil) | |
| 248 | + | var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev) | |
| 249 | + | var p xproto.ButtonPressEvent = r // alias: same type | |
| 250 | + | if p.Detail() != 3 || r.EventX() != 0x100 { | |
| 251 | + | t.Fatal("fields") | |
| 252 | + | } | |
| 253 | + | d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100} | |
| 254 | + | var out [32]byte | |
| 255 | + | d.Put(&out) | |
| 256 | + | raw[2], raw[3] = 0, 0 | |
| 257 | + | if out != raw { | |
| 258 | + | t.Fatalf("Put %x", out) | |
| 259 | + | } | |
| 260 | + | } | |
| 261 | + | ||
| 262 | + | func TestShapeEvent(t *testing.T) { | |
| 263 | + | var raw [32]byte | |
| 264 | + | raw[0], raw[1] = 64, 2 // first event 64, SK Input | |
| 265 | + | binary.LittleEndian.PutUint32(raw[4:], 0x400001) | |
| 266 | + | binary.LittleEndian.PutUint16(raw[8:], 0xfffe) | |
| 267 | + | raw[20] = 1 // shaped: after window(4) extents(8) server_time(4) | |
| 268 | + | ev := decode(t, raw[:], goldenExt()) | |
| 269 | + | if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape { | |
| 270 | + | t.Fatalf("kind %x", ev.Kind) | |
| 271 | + | } | |
| 272 | + | e := shape.NotifyOf(&ev) | |
| 273 | + | if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() { | |
| 274 | + | t.Fatal("fields") | |
| 275 | + | } | |
| 276 | + | } | |
| 277 | + | ||
| 278 | + | func TestDecodeEventRejects(t *testing.T) { | |
| 279 | + | raw := make([]byte, 64) | |
| 280 | + | for _, code := range []byte{0, 1, 0x80, 0x81} { | |
| 281 | + | raw[0] = code | |
| 282 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |
| 283 | + | t.Fatalf("code %#x accepted", code) | |
| 284 | + | } | |
| 285 | + | } | |
| 286 | + | raw[0] = 2 | |
| 287 | + | if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok { | |
| 288 | + | t.Fatal("short event accepted") | |
| 289 | + | } | |
| 290 | + | raw[0] = 35 | |
| 291 | + | binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available | |
| 292 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |
| 293 | + | t.Fatal("truncated XGE accepted") | |
| 294 | + | } | |
| 295 | + | binary.LittleEndian.PutUint32(raw[4:], 0xffffffff) | |
| 296 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |
| 297 | + | t.Fatal("huge XGE length accepted") | |
| 298 | + | } | |
| 299 | + | raw[0] = 100 // extension range, nothing negotiated | |
| 300 | + | if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown { | |
| 301 | + | t.Fatal("unknown extension event") | |
| 302 | + | } | |
| 303 | + | raw[0] = 40 // core range gap 36..63 | |
| 304 | + | if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { | |
| 305 | + | t.Fatal("unassigned core code") | |
| 306 | + | } | |
| 307 | + | raw[0] = 35 // XGE evtype >= 128 is never a known kind | |
| 308 | + | binary.LittleEndian.PutUint32(raw[4:], 0) | |
| 309 | + | raw[1] = geMajor | |
| 310 | + | binary.LittleEndian.PutUint16(raw[8:], 0x80) | |
| 311 | + | if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { | |
| 312 | + | t.Fatal("XGE evtype >= 128") | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | func TestExtTableReverseMaps(t *testing.T) { | |
| 317 | + | ext := goldenExt() | |
| 318 | + | if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 { | |
| 319 | + | t.Fatal("ByEvent CursorNotify") | |
| 320 | + | } | |
| 321 | + | if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok { | |
| 322 | + | t.Fatal("XFIXES declares two events") | |
| 323 | + | } | |
| 324 | + | if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion { | |
| 325 | + | t.Fatal("ByError") | |
| 326 | + | } | |
| 327 | + | if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes { | |
| 328 | + | t.Fatal("ByMajor") | |
| 329 | + | } | |
| 330 | + | ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries | |
| 331 | + | if _, ok := ext.ByMajor(xfixesMajor); ok { | |
| 332 | + | t.Fatal("stale major") | |
| 333 | + | } | |
| 334 | + | if _, _, ok := ext.ByEvent(xfixesEvent); ok { | |
| 335 | + | t.Fatal("stale event") | |
| 336 | + | } | |
| 337 | + | if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes { | |
| 338 | + | t.Fatal("new error base") | |
| 339 | + | } | |
| 340 | + | var render xturbo.ExtTable | |
| 341 | + | render.Set(xturbo.ExtRender, 139, 0, 142) | |
| 342 | + | for code := uint8(142); code < 147; code++ { | |
| 343 | + | if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code { | |
| 344 | + | t.Fatalf("render error %d", code) | |
| 345 | + | } | |
| 346 | + | } | |
| 347 | + | if _, _, ok := render.ByError(147); ok { | |
| 348 | + | t.Fatal("RENDER declares five errors") | |
| 349 | + | } | |
| 350 | + | } | |
| 351 | + | ||
| 352 | + | func TestErrorNames(t *testing.T) { | |
| 353 | + | if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 || | |
| 354 | + | xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" { | |
| 355 | + | t.Fatal("xproto errors") | |
| 356 | + | } | |
| 357 | + | if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" { | |
| 358 | + | t.Fatal("xfixes errors") | |
| 359 | + | } | |
| 360 | + | for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} { | |
| 361 | + | if render.ErrorName(uint8(code)) != name { | |
| 362 | + | t.Fatalf("render %d", code) | |
| 363 | + | } | |
| 364 | + | } | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | // mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes. | |
| 368 | + | func mixedEvents(t testing.TB) []byte { | |
| 369 | + | var golden [][]byte | |
| 370 | + | for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage", | |
| 371 | + | "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { | |
| 372 | + | golden = append(golden, loadEvent(t, name)) | |
| 373 | + | } | |
| 374 | + | unknown := make([]byte, 32) | |
| 375 | + | unknown[0] = 100 | |
| 376 | + | golden = append(golden, unknown) | |
| 377 | + | var stream []byte | |
| 378 | + | for i := range 10000 { | |
| 379 | + | stream = append(stream, golden[i%len(golden)]...) | |
| 380 | + | } | |
| 381 | + | return stream | |
| 382 | + | } | |
| 383 | + | ||
| 384 | + | // eventSink touches the per-kind view getters a window manager reads. | |
| 385 | + | func eventSink(ev *xturbo.Event) uint64 { | |
| 386 | + | switch ev.Kind { | |
| 387 | + | case xproto.KindMapRequest: | |
| 388 | + | e := xproto.MapRequestOf(ev) | |
| 389 | + | return uint64(e.Window()) + uint64(e.Parent()) | |
| 390 | + | case xproto.KindConfigureRequest: | |
| 391 | + | e := xproto.ConfigureRequestOf(ev) | |
| 392 | + | return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X())) | |
| 393 | + | case xproto.KindPropertyNotify: | |
| 394 | + | e := xproto.PropertyNotifyOf(ev) | |
| 395 | + | return uint64(e.Atom()) + uint64(e.State()) | |
| 396 | + | case xproto.KindClientMessage: | |
| 397 | + | e := xproto.ClientMessageOf(ev) | |
| 398 | + | return uint64(e.Type()) + uint64(e.Data().Data32()[0]) | |
| 399 | + | case xproto.KindKeymapNotify: | |
| 400 | + | return uint64(len(xproto.KeymapNotifyOf(ev).Keys())) | |
| 401 | + | case xfixes.KindSelectionNotify: | |
| 402 | + | e := xfixes.SelectionNotifyOf(ev) | |
| 403 | + | return uint64(e.Owner()) + uint64(e.Selection()) | |
| 404 | + | case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1: | |
| 405 | + | if g, ok := xproto.GeGenericOf(ev); ok { | |
| 406 | + | return uint64(len(g.Tail())) | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | return 0 | |
| 410 | + | } | |
| 411 | + | ||
| 412 | + | func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) { | |
| 413 | + | for len(stream) > 0 { | |
| 414 | + | ev, n, ok := xturbo.DecodeEvent(stream, ext) | |
| 415 | + | if !ok { | |
| 416 | + | t.Fatal("decode") | |
| 417 | + | } | |
| 418 | + | sum += eventSink(&ev) | |
| 419 | + | stream = stream[n:] | |
| 420 | + | } | |
| 421 | + | return sum | |
| 422 | + | } | |
| 423 | + | ||
| 424 | + | func TestAllocEvents(t *testing.T) { | |
| 425 | + | stream := mixedEvents(t) | |
| 426 | + | ext := goldenExt() | |
| 427 | + | var sink uint64 | |
| 428 | + | if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 { | |
| 429 | + | t.Fatalf("DecodeEvent x10k + views allocate %v times", n) | |
| 430 | + | } | |
| 431 | + | cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2} | |
| 432 | + | cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480} | |
| 433 | + | var out [32]byte | |
| 434 | + | if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 { | |
| 435 | + | t.Fatalf("EventData.Put allocates %v times", n) | |
| 436 | + | } | |
| 437 | + | b := xturbo.NewBuf(0) | |
| 438 | + | if n := testing.AllocsPerRun(1000, func() { | |
| 439 | + | b.Reset() | |
| 440 | + | cm.Put(&out) | |
| 441 | + | xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out) | |
| 442 | + | }); n != 0 { | |
| 443 | + | t.Fatalf("SendEvent allocates %v times", n) | |
| 444 | + | } | |
| 445 | + | _ = sink | |
| 446 | + | } | |
| 447 | + | ||
| 448 | + | func BenchmarkDecodeEvents10k(b *testing.B) { | |
| 449 | + | stream := mixedEvents(b) | |
| 450 | + | ext := goldenExt() | |
| 451 | + | var sink uint64 | |
| 452 | + | b.SetBytes(int64(len(stream))) | |
| 453 | + | b.ReportAllocs() | |
| 454 | + | for b.Loop() { | |
| 455 | + | sink += decodeAll(b, stream, ext) | |
| 456 | + | } | |
| 457 | + | _ = sink | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | // FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first | |
| 461 | + | // 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks | |
| 462 | + | // which extensions are negotiated and where their bases lie. | |
| 463 | + | func FuzzDecodeEvent(f *testing.F) { | |
| 464 | + | for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { | |
| 465 | + | f.Add(loadEvent(f, name), uint32(0x8a578c83)) | |
| 466 | + | } | |
| 467 | + | f.Add(make([]byte, 32), uint32(0)) | |
| 468 | + | f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) { | |
| 469 | + | var ext xturbo.ExtTable | |
| 470 | + | ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8)) | |
| 471 | + | ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0) | |
| 472 | + | ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0) | |
| 473 | + | table := &ext | |
| 474 | + | if extSeed&1 != 0 { | |
| 475 | + | table = nil | |
| 476 | + | } | |
| 477 | + | ev, n, ok := xturbo.DecodeEvent(raw, table) | |
| 478 | + | if !ok { | |
| 479 | + | if n != 0 { | |
| 480 | + | t.Fatal("n on failure") | |
| 481 | + | } | |
| 482 | + | return | |
| 483 | + | } | |
| 484 | + | if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) { | |
| 485 | + | t.Fatalf("n %d", n) | |
| 486 | + | } | |
| 487 | + | if raw[0]&0x7f == 35 { | |
| 488 | + | if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 { | |
| 489 | + | t.Fatal("XGE framing") | |
| 490 | + | } | |
| 491 | + | } else if n != 32 || ev.Tail != nil { | |
| 492 | + | t.Fatal("fixed framing") | |
| 493 | + | } | |
| 494 | + | if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) { | |
| 495 | + | t.Fatal("Kind/Ext mismatch") | |
| 496 | + | } | |
| 497 | + | _ = eventSink(&ev) | |
| 498 | + | }) | |
| 499 | + | } | |
| @@ -0,0 +1,499 @@ | |||||
| 1 | + | package xturbo_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "encoding/binary" | |||
| 6 | + | "testing" | |||
| 7 | + | ||||
| 8 | + | "bnema.dev/xturbo" | |||
| 9 | + | "bnema.dev/xturbo/internal/xtest" | |||
| 10 | + | "bnema.dev/xturbo/render" | |||
| 11 | + | "bnema.dev/xturbo/shape" | |||
| 12 | + | "bnema.dev/xturbo/xfixes" | |||
| 13 | + | "bnema.dev/xturbo/xproto" | |||
| 14 | + | ) | |||
| 15 | + | ||||
| 16 | + | // Xvfb negotiated XFIXES as major 138, first event 87, first error 140 when the | |||
| 17 | + | // goldens were captured; the BadRegion and SelectionNotify goldens confirm it. | |||
| 18 | + | const ( | |||
| 19 | + | xfixesMajor, xfixesEvent, xfixesError = 138, 87, 140 | |||
| 20 | + | geMajor = 0x83 // hand-written XGE golden | |||
| 21 | + | ) | |||
| 22 | + | ||||
| 23 | + | func loadEvent(t testing.TB, name string) []byte { | |||
| 24 | + | t.Helper() | |||
| 25 | + | raw, err := xtest.LoadHex("testdata/golden/" + name + ".event.hex") | |||
| 26 | + | if err != nil { | |||
| 27 | + | t.Fatal(err) | |||
| 28 | + | } | |||
| 29 | + | return raw | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func goldenExt() *xturbo.ExtTable { | |||
| 33 | + | var ext xturbo.ExtTable | |||
| 34 | + | ext.Set(xturbo.ExtXFixes, xfixesMajor, xfixesEvent, xfixesError) | |||
| 35 | + | ext.Set(xturbo.ExtGenericEvent, geMajor, 0, 0) | |||
| 36 | + | ext.Set(xturbo.ExtShape, 129, 64, 0) | |||
| 37 | + | return &ext | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func decode(t testing.TB, raw []byte, ext *xturbo.ExtTable) xturbo.Event { | |||
| 41 | + | t.Helper() | |||
| 42 | + | ev, n, ok := xturbo.DecodeEvent(raw, ext) | |||
| 43 | + | if !ok || n != len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) { | |||
| 44 | + | t.Fatalf("DecodeEvent(%x) = n %d ok %v", raw, n, ok) | |||
| 45 | + | } | |||
| 46 | + | return ev | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | // Expected values follow from the capture script (second client's window at | |||
| 50 | + | // 10,20 300x200, ConfigureWindow x=-5 y=40 width=640, a format-32 ClientMessage | |||
| 51 | + | // with words 0x01020304*(i+1), SetSelectionOwner PRIMARY) or from raw golden | |||
| 52 | + | // offsets defined by the X11 protocol, not from generated code. | |||
| 53 | + | func TestEventGoldens(t *testing.T) { | |||
| 54 | + | u32 := func(raw []byte, off int) uint32 { return binary.LittleEndian.Uint32(raw[off:]) } | |||
| 55 | + | ext := goldenExt() | |||
| 56 | + | core := func(name string, kind xturbo.EventKind) ([]byte, xturbo.Event) { | |||
| 57 | + | raw := loadEvent(t, "xproto/"+name) | |||
| 58 | + | ev := decode(t, raw, ext) | |||
| 59 | + | if ev.Kind != kind || ev.Ext != xturbo.ExtCore || ev.Seq != xturbo.Seq(binary.LittleEndian.Uint16(raw[2:])) || ev.Tail != nil { | |||
| 60 | + | t.Fatalf("%s: kind %d ext %d seq %d", name, ev.Kind, ev.Ext, ev.Seq) | |||
| 61 | + | } | |||
| 62 | + | return raw, ev | |||
| 63 | + | } | |||
| 64 | + | t.Run("MapRequest", func(t *testing.T) { | |||
| 65 | + | raw, ev := core("MapRequest", xproto.KindMapRequest) | |||
| 66 | + | e := xproto.MapRequestOf(&ev) | |||
| 67 | + | if ev.SendEvent || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window() != xproto.Window(u32(raw, 8)) || e.Window()&0xffff != 1 { | |||
| 68 | + | t.Fatal("fields") | |||
| 69 | + | } | |||
| 70 | + | d := xproto.MapRequestEventData{Parent: e.Parent(), Window: e.Window()} | |||
| 71 | + | var out [32]byte | |||
| 72 | + | d.Put(&out) | |||
| 73 | + | want := [32]byte(raw) | |||
| 74 | + | want[2], want[3] = 0, 0 // Put leaves the sequence to the server | |||
| 75 | + | if out != want { | |||
| 76 | + | t.Fatalf("Put %x want %x", out, want) | |||
| 77 | + | } | |||
| 78 | + | }) | |||
| 79 | + | t.Run("ConfigureRequest", func(t *testing.T) { | |||
| 80 | + | raw, ev := core("ConfigureRequest", xproto.KindConfigureRequest) | |||
| 81 | + | e := xproto.ConfigureRequestOf(&ev) | |||
| 82 | + | if e.StackMode() != xproto.StackModeAbove || e.Parent() != xproto.Window(u32(raw, 4)) || e.Window()&0xffff != 1 || | |||
| 83 | + | e.Sibling() != xproto.WindowNone || e.X() != -5 || e.Y() != 40 || e.Width() != 640 || e.Height() != 200 || | |||
| 84 | + | e.BorderWidth() != 0 || e.ValueMask() != xproto.ConfigWindowX|xproto.ConfigWindowY|xproto.ConfigWindowWidth { | |||
| 85 | + | t.Fatalf("fields x=%d y=%d w=%d h=%d mask=%d", e.X(), e.Y(), e.Width(), e.Height(), e.ValueMask()) | |||
| 86 | + | } | |||
| 87 | + | d := xproto.ConfigureRequestEventData{StackMode: e.StackMode(), Parent: e.Parent(), Window: e.Window(), Sibling: e.Sibling(), | |||
| 88 | + | X: e.X(), Y: e.Y(), Width: e.Width(), Height: e.Height(), BorderWidth: e.BorderWidth(), ValueMask: e.ValueMask()} | |||
| 89 | + | var out [32]byte | |||
| 90 | + | d.Put(&out) | |||
| 91 | + | if !bytes.Equal(out[4:], raw[4:]) || out[0] != 23 || out[1] != raw[1] { | |||
| 92 | + | t.Fatalf("Put %x", out) | |||
| 93 | + | } | |||
| 94 | + | }) | |||
| 95 | + | t.Run("PropertyNotify", func(t *testing.T) { | |||
| 96 | + | raw, ev := core("PropertyNotify", xproto.KindPropertyNotify) | |||
| 97 | + | e := xproto.PropertyNotifyOf(&ev) | |||
| 98 | + | if e.Window() != xproto.Window(u32(raw, 4)) || e.Atom() != xproto.Atom(u32(raw, 8)) || e.Atom() < 69 || | |||
| 99 | + | e.Time() != xproto.Timestamp(u32(raw, 12)) || e.State() != xproto.PropertyNewValue { | |||
| 100 | + | t.Fatal("fields") | |||
| 101 | + | } | |||
| 102 | + | }) | |||
| 103 | + | t.Run("ClientMessage", func(t *testing.T) { | |||
| 104 | + | raw, ev := core("ClientMessage", xproto.KindClientMessage) | |||
| 105 | + | e := xproto.ClientMessageOf(&ev) | |||
| 106 | + | if !ev.SendEvent || e.Format() != 32 || e.Window()&0xffff != 1 || e.Type() != xproto.Atom(u32(raw, 8)) { | |||
| 107 | + | t.Fatal("fields") | |||
| 108 | + | } | |||
| 109 | + | words := e.Data().Data32() | |||
| 110 | + | for i, w := range words { | |||
| 111 | + | if w != uint32(0x01020304*(i+1)) { | |||
| 112 | + | t.Fatalf("data32[%d] = %x", i, w) | |||
| 113 | + | } | |||
| 114 | + | } | |||
| 115 | + | d := xproto.ClientMessageEventData{Format: 32, Window: e.Window(), Type: e.Type(), Data: e.Data()} | |||
| 116 | + | var out [32]byte | |||
| 117 | + | d.Put(&out) | |||
| 118 | + | want := [32]byte(raw) | |||
| 119 | + | want[0] &^= 0x80 | |||
| 120 | + | want[2], want[3] = 0, 0 | |||
| 121 | + | if out != want { | |||
| 122 | + | t.Fatalf("Put %x want %x", out, want) | |||
| 123 | + | } | |||
| 124 | + | }) | |||
| 125 | + | t.Run("CreateNotify", func(t *testing.T) { | |||
| 126 | + | _, ev := core("CreateNotify", xproto.KindCreateNotify) | |||
| 127 | + | e := xproto.CreateNotifyOf(&ev) | |||
| 128 | + | if e.X() != 10 || e.Y() != 20 || e.Width() != 300 || e.Height() != 200 || e.OverrideRedirect() { | |||
| 129 | + | t.Fatal("fields") | |||
| 130 | + | } | |||
| 131 | + | }) | |||
| 132 | + | t.Run("Map/Unmap/DestroyNotify", func(t *testing.T) { | |||
| 133 | + | _, ev := core("MapNotify", xproto.KindMapNotify) | |||
| 134 | + | if m := xproto.MapNotifyOf(&ev); m.Window()&0xffff != 1 || m.OverrideRedirect() { | |||
| 135 | + | t.Fatal("MapNotify") | |||
| 136 | + | } | |||
| 137 | + | _, ev = core("UnmapNotify", xproto.KindUnmapNotify) | |||
| 138 | + | if u := xproto.UnmapNotifyOf(&ev); u.Window()&0xffff != 1 || u.FromConfigure() { | |||
| 139 | + | t.Fatal("UnmapNotify") | |||
| 140 | + | } | |||
| 141 | + | _, ev = core("DestroyNotify", xproto.KindDestroyNotify) | |||
| 142 | + | if d := xproto.DestroyNotifyOf(&ev); d.Window()&0xffff != 1 { | |||
| 143 | + | t.Fatal("DestroyNotify") | |||
| 144 | + | } | |||
| 145 | + | }) | |||
| 146 | + | t.Run("SelectionNotify", func(t *testing.T) { | |||
| 147 | + | _, ev := core("SelectionNotify", xproto.KindSelectionNotify) | |||
| 148 | + | e := xproto.SelectionNotifyOf(&ev) | |||
| 149 | + | if e.Requestor()&0xffff != 1 || e.Selection() != xproto.AtomSecondary || e.Target() != xproto.AtomString || e.Property() != xproto.AtomNone { | |||
| 150 | + | t.Fatal("fields") | |||
| 151 | + | } | |||
| 152 | + | }) | |||
| 153 | + | t.Run("xfixes/SelectionNotify", func(t *testing.T) { | |||
| 154 | + | raw := loadEvent(t, "xfixes/SelectionNotify") | |||
| 155 | + | ev := decode(t, raw, ext) | |||
| 156 | + | if xfixes.KindSelectionNotify != xturbo.EventKind(xturbo.ExtXFixes)<<8 { | |||
| 157 | + | t.Fatal("KindSelectionNotify value") | |||
| 158 | + | } | |||
| 159 | + | if ev.Kind != xfixes.KindSelectionNotify || ev.Ext != xturbo.ExtXFixes { | |||
| 160 | + | t.Fatalf("kind %x", ev.Kind) | |||
| 161 | + | } | |||
| 162 | + | e := xfixes.SelectionNotifyOf(&ev) | |||
| 163 | + | if e.Subtype() != xfixes.SelectionEventSetSelectionOwner || e.Window() != xproto.Window(u32(raw, 4)) || | |||
| 164 | + | e.Owner()&0xffff != 1 || e.Selection() != xproto.AtomPrimary || e.Timestamp() != e.SelectionTimestamp() { | |||
| 165 | + | t.Fatal("fields") | |||
| 166 | + | } | |||
| 167 | + | // Without XFIXES negotiated the same bytes are an unknown event. | |||
| 168 | + | if ev, _, ok := xturbo.DecodeEvent(raw, nil); !ok || ev.Kind != xturbo.KindUnknown { | |||
| 169 | + | t.Fatal("nil ext must decode as unknown") | |||
| 170 | + | } | |||
| 171 | + | }) | |||
| 172 | + | t.Run("KeymapNotify", func(t *testing.T) { | |||
| 173 | + | raw := loadEvent(t, "xproto/KeymapNotify") | |||
| 174 | + | ev := decode(t, raw, ext) | |||
| 175 | + | if ev.Kind != xproto.KindKeymapNotify || ev.Seq != 0 { | |||
| 176 | + | t.Fatalf("kind %d seq %d", ev.Kind, ev.Seq) | |||
| 177 | + | } | |||
| 178 | + | e := xproto.KeymapNotifyOf(&ev) | |||
| 179 | + | if !bytes.Equal(e.Keys(), raw[1:]) { | |||
| 180 | + | t.Fatal("keys") | |||
| 181 | + | } | |||
| 182 | + | d := xproto.KeymapNotifyEventData{Keys: [31]uint8(raw[1:])} | |||
| 183 | + | var out [32]byte | |||
| 184 | + | d.Put(&out) | |||
| 185 | + | if !bytes.Equal(out[:], raw) { | |||
| 186 | + | t.Fatalf("Put %x", out) | |||
| 187 | + | } | |||
| 188 | + | }) | |||
| 189 | + | t.Run("GeGeneric", func(t *testing.T) { | |||
| 190 | + | raw := loadEvent(t, "xproto/GeGeneric") | |||
| 191 | + | ev := decode(t, raw, ext) | |||
| 192 | + | if ev.Kind != xturbo.EventKind(xturbo.ExtGenericEvent)<<8|0x80|1 || ev.Ext != xturbo.ExtGenericEvent || ev.Seq != 5 || | |||
| 193 | + | !bytes.Equal(ev.Tail, raw[32:]) { | |||
| 194 | + | t.Fatalf("kind %x tail %x", ev.Kind, ev.Tail) | |||
| 195 | + | } | |||
| 196 | + | g, ok := xproto.GeGenericOf(&ev) | |||
| 197 | + | if !ok || g.Extension() != geMajor || g.Length() != 2 || g.EventType() != 1 || !bytes.Equal(g.Tail(), raw[32:]) { | |||
| 198 | + | t.Fatal("view") | |||
| 199 | + | } | |||
| 200 | + | // Unknown major: still framed so Dispatch skips the tail. | |||
| 201 | + | ev, n, ok := xturbo.DecodeEvent(raw, nil) | |||
| 202 | + | if !ok || n != 40 || ev.Kind != xturbo.KindUnknown || len(ev.Tail) != 8 { | |||
| 203 | + | t.Fatal("unknown XGE framing") | |||
| 204 | + | } | |||
| 205 | + | ev.Tail = ev.Tail[:4] | |||
| 206 | + | if _, ok := xproto.GeGenericOf(&ev); ok { | |||
| 207 | + | t.Fatal("short Tail accepted") | |||
| 208 | + | } | |||
| 209 | + | }) | |||
| 210 | + | } | |||
| 211 | + | ||||
| 212 | + | // TestEventDataPutInPlace re-encodes events into the Event.Raw their fields | |||
| 213 | + | // were read from. ClientMessage's Data is a union view over Raw; KeymapNotify's | |||
| 214 | + | // Keys is a slice view over Raw. Put must read every field before writing dst. | |||
| 215 | + | func TestEventDataPutInPlace(t *testing.T) { | |||
| 216 | + | raw := loadEvent(t, "xproto/ClientMessage") | |||
| 217 | + | ev := decode(t, raw, nil) | |||
| 218 | + | e := xproto.ClientMessageOf(&ev) | |||
| 219 | + | d := xproto.ClientMessageEventData{Format: e.Format(), Window: e.Window(), Type: e.Type(), Data: e.Data()} | |||
| 220 | + | d.Put(&ev.Raw) | |||
| 221 | + | want := [32]byte(raw) | |||
| 222 | + | want[0] &^= 0x80 | |||
| 223 | + | want[2], want[3] = 0, 0 | |||
| 224 | + | if ev.Raw != want { | |||
| 225 | + | t.Fatalf("ClientMessage in place %x want %x", ev.Raw, want) | |||
| 226 | + | } | |||
| 227 | + | if n := testing.AllocsPerRun(1000, func() { d.Data = e.Data(); d.Put(&ev.Raw) }); n != 0 { | |||
| 228 | + | t.Fatalf("in-place Put allocates %v times", n) | |||
| 229 | + | } | |||
| 230 | + | raw = loadEvent(t, "xproto/KeymapNotify") | |||
| 231 | + | ev = decode(t, raw, nil) | |||
| 232 | + | keys := xproto.KeymapNotifyOf(&ev).Keys() | |||
| 233 | + | k := xproto.KeymapNotifyEventData{Keys: [31]uint8(keys)} | |||
| 234 | + | k.Put(&ev.Raw) | |||
| 235 | + | if ev.Raw != [32]byte(raw) || !bytes.Equal(keys, raw[1:]) { | |||
| 236 | + | t.Fatalf("KeymapNotify in place %x", ev.Raw) | |||
| 237 | + | } | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | func TestEventCopyAliases(t *testing.T) { | |||
| 241 | + | if xproto.KindButtonRelease != 5 || xproto.KindKeyRelease != 3 || xproto.KindLeaveNotify != 8 || xproto.KindFocusOut != 10 || xproto.KindCirculateRequest != 27 { | |||
| 242 | + | t.Fatal("eventcopy kinds") | |||
| 243 | + | } | |||
| 244 | + | var raw [32]byte | |||
| 245 | + | raw[0], raw[1] = 5, 3 // ButtonRelease, button 3 | |||
| 246 | + | binary.LittleEndian.PutUint16(raw[24:], 0x100) | |||
| 247 | + | ev := decode(t, raw[:], nil) | |||
| 248 | + | var r xproto.ButtonReleaseEvent = xproto.ButtonReleaseOf(&ev) | |||
| 249 | + | var p xproto.ButtonPressEvent = r // alias: same type | |||
| 250 | + | if p.Detail() != 3 || r.EventX() != 0x100 { | |||
| 251 | + | t.Fatal("fields") | |||
| 252 | + | } | |||
| 253 | + | d := xproto.ButtonReleaseEventData{Detail: 3, EventX: 0x100} | |||
| 254 | + | var out [32]byte | |||
| 255 | + | d.Put(&out) | |||
| 256 | + | raw[2], raw[3] = 0, 0 | |||
| 257 | + | if out != raw { | |||
| 258 | + | t.Fatalf("Put %x", out) | |||
| 259 | + | } | |||
| 260 | + | } | |||
| 261 | + | ||||
| 262 | + | func TestShapeEvent(t *testing.T) { | |||
| 263 | + | var raw [32]byte | |||
| 264 | + | raw[0], raw[1] = 64, 2 // first event 64, SK Input | |||
| 265 | + | binary.LittleEndian.PutUint32(raw[4:], 0x400001) | |||
| 266 | + | binary.LittleEndian.PutUint16(raw[8:], 0xfffe) | |||
| 267 | + | raw[20] = 1 // shaped: after window(4) extents(8) server_time(4) | |||
| 268 | + | ev := decode(t, raw[:], goldenExt()) | |||
| 269 | + | if ev.Kind != shape.KindNotify || ev.Ext != xturbo.ExtShape { | |||
| 270 | + | t.Fatalf("kind %x", ev.Kind) | |||
| 271 | + | } | |||
| 272 | + | e := shape.NotifyOf(&ev) | |||
| 273 | + | if e.ShapeKind() != shape.SkInput || e.AffectedWindow() != 0x400001 || e.ExtentsX() != -2 || !e.Shaped() { | |||
| 274 | + | t.Fatal("fields") | |||
| 275 | + | } | |||
| 276 | + | } | |||
| 277 | + | ||||
| 278 | + | func TestDecodeEventRejects(t *testing.T) { | |||
| 279 | + | raw := make([]byte, 64) | |||
| 280 | + | for _, code := range []byte{0, 1, 0x80, 0x81} { | |||
| 281 | + | raw[0] = code | |||
| 282 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |||
| 283 | + | t.Fatalf("code %#x accepted", code) | |||
| 284 | + | } | |||
| 285 | + | } | |||
| 286 | + | raw[0] = 2 | |||
| 287 | + | if _, _, ok := xturbo.DecodeEvent(raw[:31], nil); ok { | |||
| 288 | + | t.Fatal("short event accepted") | |||
| 289 | + | } | |||
| 290 | + | raw[0] = 35 | |||
| 291 | + | binary.LittleEndian.PutUint32(raw[4:], 9) // 36 tail bytes > 32 available | |||
| 292 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |||
| 293 | + | t.Fatal("truncated XGE accepted") | |||
| 294 | + | } | |||
| 295 | + | binary.LittleEndian.PutUint32(raw[4:], 0xffffffff) | |||
| 296 | + | if _, _, ok := xturbo.DecodeEvent(raw, nil); ok { | |||
| 297 | + | t.Fatal("huge XGE length accepted") | |||
| 298 | + | } | |||
| 299 | + | raw[0] = 100 // extension range, nothing negotiated | |||
| 300 | + | if ev, n, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || n != 32 || ev.Kind != xturbo.KindUnknown { | |||
| 301 | + | t.Fatal("unknown extension event") | |||
| 302 | + | } | |||
| 303 | + | raw[0] = 40 // core range gap 36..63 | |||
| 304 | + | if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { | |||
| 305 | + | t.Fatal("unassigned core code") | |||
| 306 | + | } | |||
| 307 | + | raw[0] = 35 // XGE evtype >= 128 is never a known kind | |||
| 308 | + | binary.LittleEndian.PutUint32(raw[4:], 0) | |||
| 309 | + | raw[1] = geMajor | |||
| 310 | + | binary.LittleEndian.PutUint16(raw[8:], 0x80) | |||
| 311 | + | if ev, _, ok := xturbo.DecodeEvent(raw, goldenExt()); !ok || ev.Kind != xturbo.KindUnknown { | |||
| 312 | + | t.Fatal("XGE evtype >= 128") | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 315 | + | ||||
| 316 | + | func TestExtTableReverseMaps(t *testing.T) { | |||
| 317 | + | ext := goldenExt() | |||
| 318 | + | if id, rel, ok := ext.ByEvent(xfixesEvent + 1); !ok || id != xturbo.ExtXFixes || rel != 1 { | |||
| 319 | + | t.Fatal("ByEvent CursorNotify") | |||
| 320 | + | } | |||
| 321 | + | if _, _, ok := ext.ByEvent(xfixesEvent + 2); ok { | |||
| 322 | + | t.Fatal("XFIXES declares two events") | |||
| 323 | + | } | |||
| 324 | + | if id, rel, ok := ext.ByError(xfixesError); !ok || id != xturbo.ExtXFixes || rel != xfixes.BadRegion { | |||
| 325 | + | t.Fatal("ByError") | |||
| 326 | + | } | |||
| 327 | + | if id, ok := ext.ByMajor(xfixesMajor); !ok || id != xturbo.ExtXFixes { | |||
| 328 | + | t.Fatal("ByMajor") | |||
| 329 | + | } | |||
| 330 | + | ext.Set(xturbo.ExtXFixes, 140, 90, 150) // re-negotiation drops stale entries | |||
| 331 | + | if _, ok := ext.ByMajor(xfixesMajor); ok { | |||
| 332 | + | t.Fatal("stale major") | |||
| 333 | + | } | |||
| 334 | + | if _, _, ok := ext.ByEvent(xfixesEvent); ok { | |||
| 335 | + | t.Fatal("stale event") | |||
| 336 | + | } | |||
| 337 | + | if id, _, ok := ext.ByError(150); !ok || id != xturbo.ExtXFixes { | |||
| 338 | + | t.Fatal("new error base") | |||
| 339 | + | } | |||
| 340 | + | var render xturbo.ExtTable | |||
| 341 | + | render.Set(xturbo.ExtRender, 139, 0, 142) | |||
| 342 | + | for code := uint8(142); code < 147; code++ { | |||
| 343 | + | if id, rel, ok := render.ByError(code); !ok || id != xturbo.ExtRender || render.Info(id).FirstError+rel != code { | |||
| 344 | + | t.Fatalf("render error %d", code) | |||
| 345 | + | } | |||
| 346 | + | } | |||
| 347 | + | if _, _, ok := render.ByError(147); ok { | |||
| 348 | + | t.Fatal("RENDER declares five errors") | |||
| 349 | + | } | |||
| 350 | + | } | |||
| 351 | + | ||||
| 352 | + | func TestErrorNames(t *testing.T) { | |||
| 353 | + | if xproto.BadWindow != 3 || xproto.BadAtom != 5 || xproto.BadImplementation != 17 || | |||
| 354 | + | xproto.ErrorName(xproto.BadWindow) != "BadWindow" || xproto.ErrorName(0) != "" || xproto.ErrorName(200) != "" { | |||
| 355 | + | t.Fatal("xproto errors") | |||
| 356 | + | } | |||
| 357 | + | if xfixes.BadRegion != 0 || xfixes.ErrorName(0) != "BadRegion" { | |||
| 358 | + | t.Fatal("xfixes errors") | |||
| 359 | + | } | |||
| 360 | + | for code, name := range []string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} { | |||
| 361 | + | if render.ErrorName(uint8(code)) != name { | |||
| 362 | + | t.Fatalf("render %d", code) | |||
| 363 | + | } | |||
| 364 | + | } | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | // mixedEvents is 10k events: core, xfixes, XGE with a tail and unknown codes. | |||
| 368 | + | func mixedEvents(t testing.TB) []byte { | |||
| 369 | + | var golden [][]byte | |||
| 370 | + | for _, name := range []string{"xproto/MapRequest", "xproto/ConfigureRequest", "xproto/PropertyNotify", "xproto/ClientMessage", | |||
| 371 | + | "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { | |||
| 372 | + | golden = append(golden, loadEvent(t, name)) | |||
| 373 | + | } | |||
| 374 | + | unknown := make([]byte, 32) | |||
| 375 | + | unknown[0] = 100 | |||
| 376 | + | golden = append(golden, unknown) | |||
| 377 | + | var stream []byte | |||
| 378 | + | for i := range 10000 { | |||
| 379 | + | stream = append(stream, golden[i%len(golden)]...) | |||
| 380 | + | } | |||
| 381 | + | return stream | |||
| 382 | + | } | |||
| 383 | + | ||||
| 384 | + | // eventSink touches the per-kind view getters a window manager reads. | |||
| 385 | + | func eventSink(ev *xturbo.Event) uint64 { | |||
| 386 | + | switch ev.Kind { | |||
| 387 | + | case xproto.KindMapRequest: | |||
| 388 | + | e := xproto.MapRequestOf(ev) | |||
| 389 | + | return uint64(e.Window()) + uint64(e.Parent()) | |||
| 390 | + | case xproto.KindConfigureRequest: | |||
| 391 | + | e := xproto.ConfigureRequestOf(ev) | |||
| 392 | + | return uint64(e.Window()) + uint64(e.Width()) + uint64(e.ValueMask()) + uint64(int64(e.X())) | |||
| 393 | + | case xproto.KindPropertyNotify: | |||
| 394 | + | e := xproto.PropertyNotifyOf(ev) | |||
| 395 | + | return uint64(e.Atom()) + uint64(e.State()) | |||
| 396 | + | case xproto.KindClientMessage: | |||
| 397 | + | e := xproto.ClientMessageOf(ev) | |||
| 398 | + | return uint64(e.Type()) + uint64(e.Data().Data32()[0]) | |||
| 399 | + | case xproto.KindKeymapNotify: | |||
| 400 | + | return uint64(len(xproto.KeymapNotifyOf(ev).Keys())) | |||
| 401 | + | case xfixes.KindSelectionNotify: | |||
| 402 | + | e := xfixes.SelectionNotifyOf(ev) | |||
| 403 | + | return uint64(e.Owner()) + uint64(e.Selection()) | |||
| 404 | + | case xturbo.EventKind(xturbo.ExtGenericEvent)<<8 | 0x80 | 1: | |||
| 405 | + | if g, ok := xproto.GeGenericOf(ev); ok { | |||
| 406 | + | return uint64(len(g.Tail())) | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | return 0 | |||
| 410 | + | } | |||
| 411 | + | ||||
| 412 | + | func decodeAll(t testing.TB, stream []byte, ext *xturbo.ExtTable) (sum uint64) { | |||
| 413 | + | for len(stream) > 0 { | |||
| 414 | + | ev, n, ok := xturbo.DecodeEvent(stream, ext) | |||
| 415 | + | if !ok { | |||
| 416 | + | t.Fatal("decode") | |||
| 417 | + | } | |||
| 418 | + | sum += eventSink(&ev) | |||
| 419 | + | stream = stream[n:] | |||
| 420 | + | } | |||
| 421 | + | return sum | |||
| 422 | + | } | |||
| 423 | + | ||||
| 424 | + | func TestAllocEvents(t *testing.T) { | |||
| 425 | + | stream := mixedEvents(t) | |||
| 426 | + | ext := goldenExt() | |||
| 427 | + | var sink uint64 | |||
| 428 | + | if n := testing.AllocsPerRun(10, func() { sink += decodeAll(t, stream, ext) }); n != 0 { | |||
| 429 | + | t.Fatalf("DecodeEvent x10k + views allocate %v times", n) | |||
| 430 | + | } | |||
| 431 | + | cm := xproto.ClientMessageEventData{Format: 32, Window: 1, Type: 2} | |||
| 432 | + | cfg := xproto.ConfigureNotifyEventData{Window: 1, Width: 640, Height: 480} | |||
| 433 | + | var out [32]byte | |||
| 434 | + | if n := testing.AllocsPerRun(1000, func() { cm.Put(&out); cfg.Put(&out) }); n != 0 { | |||
| 435 | + | t.Fatalf("EventData.Put allocates %v times", n) | |||
| 436 | + | } | |||
| 437 | + | b := xturbo.NewBuf(0) | |||
| 438 | + | if n := testing.AllocsPerRun(1000, func() { | |||
| 439 | + | b.Reset() | |||
| 440 | + | cm.Put(&out) | |||
| 441 | + | xproto.SendEvent(b, false, 1, xproto.EventMaskSubstructureRedirect, &out) | |||
| 442 | + | }); n != 0 { | |||
| 443 | + | t.Fatalf("SendEvent allocates %v times", n) | |||
| 444 | + | } | |||
| 445 | + | _ = sink | |||
| 446 | + | } | |||
| 447 | + | ||||
| 448 | + | func BenchmarkDecodeEvents10k(b *testing.B) { | |||
| 449 | + | stream := mixedEvents(b) | |||
| 450 | + | ext := goldenExt() | |||
| 451 | + | var sink uint64 | |||
| 452 | + | b.SetBytes(int64(len(stream))) | |||
| 453 | + | b.ReportAllocs() | |||
| 454 | + | for b.Loop() { | |||
| 455 | + | sink += decodeAll(b, stream, ext) | |||
| 456 | + | } | |||
| 457 | + | _ = sink | |||
| 458 | + | } | |||
| 459 | + | ||||
| 460 | + | // FuzzDecodeEvent: no panic; n is 32 or the XGE length; Raw copies the first | |||
| 461 | + | // 32 bytes; Tail stays within raw; Kind is consistent with Ext. extSeed picks | |||
| 462 | + | // which extensions are negotiated and where their bases lie. | |||
| 463 | + | func FuzzDecodeEvent(f *testing.F) { | |||
| 464 | + | for _, name := range []string{"xproto/MapRequest", "xproto/ClientMessage", "xproto/KeymapNotify", "xfixes/SelectionNotify", "xproto/GeGeneric"} { | |||
| 465 | + | f.Add(loadEvent(f, name), uint32(0x8a578c83)) | |||
| 466 | + | } | |||
| 467 | + | f.Add(make([]byte, 32), uint32(0)) | |||
| 468 | + | f.Fuzz(func(t *testing.T, raw []byte, extSeed uint32) { | |||
| 469 | + | var ext xturbo.ExtTable | |||
| 470 | + | ext.Set(xturbo.ExtXFixes, uint8(extSeed>>24), uint8(extSeed>>16), uint8(extSeed>>8)) | |||
| 471 | + | ext.Set(xturbo.ExtGenericEvent, uint8(extSeed), 0, 0) | |||
| 472 | + | ext.Set(xturbo.ExtShape, uint8(extSeed>>4), uint8(extSeed>>12)|64, 0) | |||
| 473 | + | table := &ext | |||
| 474 | + | if extSeed&1 != 0 { | |||
| 475 | + | table = nil | |||
| 476 | + | } | |||
| 477 | + | ev, n, ok := xturbo.DecodeEvent(raw, table) | |||
| 478 | + | if !ok { | |||
| 479 | + | if n != 0 { | |||
| 480 | + | t.Fatal("n on failure") | |||
| 481 | + | } | |||
| 482 | + | return | |||
| 483 | + | } | |||
| 484 | + | if len(raw) < 32 || raw[0]&0x7f < 2 || n > len(raw) || !bytes.Equal(ev.Raw[:], raw[:32]) || ev.SendEvent != (raw[0]&0x80 != 0) { | |||
| 485 | + | t.Fatalf("n %d", n) | |||
| 486 | + | } | |||
| 487 | + | if raw[0]&0x7f == 35 { | |||
| 488 | + | if n != 32+4*int(binary.LittleEndian.Uint32(raw[4:])) || len(ev.Tail) != n-32 { | |||
| 489 | + | t.Fatal("XGE framing") | |||
| 490 | + | } | |||
| 491 | + | } else if n != 32 || ev.Tail != nil { | |||
| 492 | + | t.Fatal("fixed framing") | |||
| 493 | + | } | |||
| 494 | + | if ev.Kind != xturbo.KindUnknown && ev.Kind>>8 != xturbo.EventKind(ev.Ext) { | |||
| 495 | + | t.Fatal("Kind/Ext mismatch") | |||
| 496 | + | } | |||
| 497 | + | _ = eventSink(&ev) | |||
| 498 | + | }) | |||
| 499 | + | } | |||
M ext.go +66 -4
| @@ -7,12 +7,47 @@ type ExtInfo struct { | |||
| 7 | 7 | } | |
| 8 | 8 | ||
| 9 | 9 | // ExtTable is populated during setup. Fixed arrays avoid lookup allocations. | |
| 10 | - | // Reverse mapping is completed with the read-side runtime in Phase 3. | |
| 11 | - | type ExtTable struct{ info [extCount]ExtInfo } | |
| 10 | + | // Reverse maps store ExtID+1 so the zero value means "none". | |
| 11 | + | type ExtTable struct { | |
| 12 | + | info [extCount]ExtInfo | |
| 13 | + | byEvent [128]uint8 | |
| 14 | + | byError [256]uint8 | |
| 15 | + | byMajor [256]uint8 | |
| 16 | + | } | |
| 12 | 17 | ||
| 18 | + | // Set records an extension's negotiated opcodes and fills the reverse maps | |
| 19 | + | // for the event and error codes the extension declares in the pinned XML. | |
| 13 | 20 | func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) { | |
| 14 | - | if id < extCount { | |
| 15 | - | t.info[id] = ExtInfo{true, major, firstEvent, firstError} | |
| 21 | + | if id == ExtCore || id >= extCount { | |
| 22 | + | return | |
| 23 | + | } | |
| 24 | + | tag := uint8(id) + 1 | |
| 25 | + | for i := range t.byEvent { | |
| 26 | + | if t.byEvent[i] == tag { | |
| 27 | + | t.byEvent[i] = 0 | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | for i := range t.byError { | |
| 31 | + | if t.byError[i] == tag { | |
| 32 | + | t.byError[i] = 0 | |
| 33 | + | } | |
| 34 | + | if t.byMajor[i] == tag { | |
| 35 | + | t.byMajor[i] = 0 | |
| 36 | + | } | |
| 37 | + | } | |
| 38 | + | t.info[id] = ExtInfo{true, major, firstEvent, firstError} | |
| 39 | + | if major != 0 { | |
| 40 | + | t.byMajor[major] = tag | |
| 41 | + | } | |
| 42 | + | if firstEvent != 0 { | |
| 43 | + | for i := 0; i < int(extEvents[id]) && int(firstEvent)+i < len(t.byEvent); i++ { | |
| 44 | + | t.byEvent[int(firstEvent)+i] = tag | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | if firstError != 0 { | |
| 48 | + | for i := 0; i < int(extErrors[id]) && int(firstError)+i < len(t.byError); i++ { | |
| 49 | + | t.byError[int(firstError)+i] = tag | |
| 50 | + | } | |
| 16 | 51 | } | |
| 17 | 52 | } | |
| 18 | 53 | func (t *ExtTable) Info(id ExtID) ExtInfo { | |
| @@ -25,3 +60,30 @@ func (t *ExtTable) Info(id ExtID) ExtInfo { | |||
| 25 | 60 | return t.info[id] | |
| 26 | 61 | } | |
| 27 | 62 | func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major } | |
| 63 | + | ||
| 64 | + | // ByEvent maps an event code (raw[0]&0x7f) to its extension and the code | |
| 65 | + | // relative to the extension's first event. | |
| 66 | + | func (t *ExtTable) ByEvent(code uint8) (id ExtID, rel uint8, ok bool) { | |
| 67 | + | if code >= 128 || t.byEvent[code] == 0 { | |
| 68 | + | return ExtCore, 0, false | |
| 69 | + | } | |
| 70 | + | id = ExtID(t.byEvent[code] - 1) | |
| 71 | + | return id, code - t.info[id].FirstEvent, true | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // ByError maps a wire error code to its extension and relative error code. | |
| 75 | + | func (t *ExtTable) ByError(code uint8) (id ExtID, rel uint8, ok bool) { | |
| 76 | + | if t.byError[code] == 0 { | |
| 77 | + | return ExtCore, 0, false | |
| 78 | + | } | |
| 79 | + | id = ExtID(t.byError[code] - 1) | |
| 80 | + | return id, code - t.info[id].FirstError, true | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | // ByMajor maps a major opcode to its extension. | |
| 84 | + | func (t *ExtTable) ByMajor(major uint8) (id ExtID, ok bool) { | |
| 85 | + | if t.byMajor[major] == 0 { | |
| 86 | + | return ExtCore, false | |
| 87 | + | } | |
| 88 | + | return ExtID(t.byMajor[major] - 1), true | |
| 89 | + | } | |
| @@ -7,12 +7,47 @@ type ExtInfo struct { | |||||
| 7 | } | 7 | } | ||
| 8 | 8 | ||||
| 9 | // ExtTable is populated during setup. Fixed arrays avoid lookup allocations. | 9 | // ExtTable is populated during setup. Fixed arrays avoid lookup allocations. | ||
| 10 | - | // Reverse mapping is completed with the read-side runtime in Phase 3. | 10 | + | // Reverse maps store ExtID+1 so the zero value means "none". |
| 11 | - | type ExtTable struct{ info [extCount]ExtInfo } | 11 | + | type ExtTable struct { |
| 12 | + | info [extCount]ExtInfo | |||
| 13 | + | byEvent [128]uint8 | |||
| 14 | + | byError [256]uint8 | |||
| 15 | + | byMajor [256]uint8 | |||
| 16 | + | } | |||
| 12 | 17 | ||||
| 18 | + | // Set records an extension's negotiated opcodes and fills the reverse maps | |||
| 19 | + | // for the event and error codes the extension declares in the pinned XML. | |||
| 13 | func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) { | 20 | func (t *ExtTable) Set(id ExtID, major, firstEvent, firstError uint8) { | ||
| 14 | - | if id < extCount { | 21 | + | if id == ExtCore || id >= extCount { |
| 15 | - | t.info[id] = ExtInfo{true, major, firstEvent, firstError} | 22 | + | return |
| 23 | + | } | |||
| 24 | + | tag := uint8(id) + 1 | |||
| 25 | + | for i := range t.byEvent { | |||
| 26 | + | if t.byEvent[i] == tag { | |||
| 27 | + | t.byEvent[i] = 0 | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | for i := range t.byError { | |||
| 31 | + | if t.byError[i] == tag { | |||
| 32 | + | t.byError[i] = 0 | |||
| 33 | + | } | |||
| 34 | + | if t.byMajor[i] == tag { | |||
| 35 | + | t.byMajor[i] = 0 | |||
| 36 | + | } | |||
| 37 | + | } | |||
| 38 | + | t.info[id] = ExtInfo{true, major, firstEvent, firstError} | |||
| 39 | + | if major != 0 { | |||
| 40 | + | t.byMajor[major] = tag | |||
| 41 | + | } | |||
| 42 | + | if firstEvent != 0 { | |||
| 43 | + | for i := 0; i < int(extEvents[id]) && int(firstEvent)+i < len(t.byEvent); i++ { | |||
| 44 | + | t.byEvent[int(firstEvent)+i] = tag | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | if firstError != 0 { | |||
| 48 | + | for i := 0; i < int(extErrors[id]) && int(firstError)+i < len(t.byError); i++ { | |||
| 49 | + | t.byError[int(firstError)+i] = tag | |||
| 50 | + | } | |||
| 16 | } | 51 | } | ||
| 17 | } | 52 | } | ||
| 18 | func (t *ExtTable) Info(id ExtID) ExtInfo { | 53 | func (t *ExtTable) Info(id ExtID) ExtInfo { | ||
| @@ -25,3 +60,30 @@ func (t *ExtTable) Info(id ExtID) ExtInfo { | |||||
| 25 | return t.info[id] | 60 | return t.info[id] | ||
| 26 | } | 61 | } | ||
| 27 | func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major } | 62 | func (t *ExtTable) Major(id ExtID) uint8 { return t.Info(id).Major } | ||
| 63 | + | ||||
| 64 | + | // ByEvent maps an event code (raw[0]&0x7f) to its extension and the code | |||
| 65 | + | // relative to the extension's first event. | |||
| 66 | + | func (t *ExtTable) ByEvent(code uint8) (id ExtID, rel uint8, ok bool) { | |||
| 67 | + | if code >= 128 || t.byEvent[code] == 0 { | |||
| 68 | + | return ExtCore, 0, false | |||
| 69 | + | } | |||
| 70 | + | id = ExtID(t.byEvent[code] - 1) | |||
| 71 | + | return id, code - t.info[id].FirstEvent, true | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // ByError maps a wire error code to its extension and relative error code. | |||
| 75 | + | func (t *ExtTable) ByError(code uint8) (id ExtID, rel uint8, ok bool) { | |||
| 76 | + | if t.byError[code] == 0 { | |||
| 77 | + | return ExtCore, 0, false | |||
| 78 | + | } | |||
| 79 | + | id = ExtID(t.byError[code] - 1) | |||
| 80 | + | return id, code - t.info[id].FirstError, true | |||
| 81 | + | } | |||
| 82 | + | ||||
| 83 | + | // ByMajor maps a major opcode to its extension. | |||
| 84 | + | func (t *ExtTable) ByMajor(major uint8) (id ExtID, ok bool) { | |||
| 85 | + | if t.byMajor[major] == 0 { | |||
| 86 | + | return ExtCore, false | |||
| 87 | + | } | |||
| 88 | + | return ExtID(t.byMajor[major] - 1), true | |||
| 89 | + | } | |||
M ext_generated.go +5 -0
| @@ -17,3 +17,8 @@ const ( | |||
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | 19 | var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"} | |
| 20 | + | ||
| 21 | + | // extEvents and extErrors are the event and error code ranges each extension | |
| 22 | + | // declares in the pinned XML; ExtTable.Set uses them to fill reverse maps. | |
| 23 | + | var extEvents = [extCount]uint8{0, 0, 0, 0, 0, 2, 0, 0, 1} | |
| 24 | + | var extErrors = [extCount]uint8{0, 0, 0, 0, 0, 1, 0, 5, 0} | |
| @@ -17,3 +17,8 @@ const ( | |||||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"} | 19 | var ExtXName = [extCount]string{"", "BIG-REQUESTS", "Generic Event Extension", "XC-MISC", "Composite", "XFIXES", "X-Resource", "RENDER", "SHAPE"} | ||
| 20 | + | ||||
| 21 | + | // extEvents and extErrors are the event and error code ranges each extension | |||
| 22 | + | // declares in the pinned XML; ExtTable.Set uses them to fill reverse maps. | |||
| 23 | + | var extEvents = [extCount]uint8{0, 0, 0, 0, 0, 2, 0, 0, 1} | |||
| 24 | + | var extErrors = [extCount]uint8{0, 0, 0, 0, 0, 1, 0, 5, 0} | |||
M internal/xtest/README.md +33 -1
| @@ -1,6 +1,27 @@ | |||
| 1 | 1 | # Wire goldens | |
| 2 | 2 | ||
| 3 | - | Refresh the request fixtures from the repository root: | |
| 3 | + | `make xserver` captures from a fresh Xvfb and compares every packet with the | |
| 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: | |
| 4 | 25 | ||
| 5 | 26 | ``` | |
| 6 | 27 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | |
| @@ -38,3 +59,14 @@ adds GetWindowAttributes (child window), QueryTree (root), an unset WM_NAME | |||
| 38 | 59 | GetProperty (zero-length value) and ListExtensions (variable STR list). These | |
| 39 | 60 | reply-only requests use opcodes outside the request-golden classifier. | |
| 40 | 61 | Generated `FuzzParse*Reply` targets seed from every matching reply golden. | |
| 62 | + | ||
| 63 | + | T2.7 adds event and error goldens (`<Name>.event.hex`, `<Name>.error.hex`). | |
| 64 | + | The tee client selects SubstructureRedirect/Notify and PropertyChange on the | |
| 65 | + | root plus XFIXES selection input; a second raw client (not teed) creates, maps, | |
| 66 | + | configures, unmaps and destroys a window, writes a root property, sends a | |
| 67 | + | format-32 ClientMessage and takes PRIMARY. ConvertSelection of an unowned | |
| 68 | + | SECONDARY yields a core SelectionNotify. GetAtomName, GetWindowAttributes and | |
| 69 | + | XFIXES DestroyRegion on bad IDs yield BadAtom, BadWindow and BadRegion. Each | |
| 70 | + | batch is fenced by a GetInputFocus reply; unexpected packets fail capture. | |
| 71 | + | `KeymapNotify` and `GeGeneric` (XGE framing with a tail) are hand-written | |
| 72 | + | (`# hand:`): Xvfb produces neither for these clients. | |
| @@ -1,6 +1,27 @@ | |||||
| 1 | # Wire goldens | 1 | # Wire goldens | ||
| 2 | 2 | ||||
| 3 | - | Refresh the request fixtures from the repository root: | 3 | + | `make xserver` captures from a fresh Xvfb and compares every packet with the |
| 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: | |||
| 4 | 25 | ||||
| 5 | ``` | 26 | ``` | ||
| 6 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | 27 | PATH="$PATH:/usr/sbin" go test -tags xserver ./internal/xtest -update | ||
| @@ -38,3 +59,14 @@ adds GetWindowAttributes (child window), QueryTree (root), an unset WM_NAME | |||||
| 38 | GetProperty (zero-length value) and ListExtensions (variable STR list). These | 59 | GetProperty (zero-length value) and ListExtensions (variable STR list). These | ||
| 39 | reply-only requests use opcodes outside the request-golden classifier. | 60 | reply-only requests use opcodes outside the request-golden classifier. | ||
| 40 | Generated `FuzzParse*Reply` targets seed from every matching reply golden. | 61 | Generated `FuzzParse*Reply` targets seed from every matching reply golden. | ||
| 62 | + | ||||
| 63 | + | T2.7 adds event and error goldens (`<Name>.event.hex`, `<Name>.error.hex`). | |||
| 64 | + | The tee client selects SubstructureRedirect/Notify and PropertyChange on the | |||
| 65 | + | root plus XFIXES selection input; a second raw client (not teed) creates, maps, | |||
| 66 | + | configures, unmaps and destroys a window, writes a root property, sends a | |||
| 67 | + | format-32 ClientMessage and takes PRIMARY. ConvertSelection of an unowned | |||
| 68 | + | SECONDARY yields a core SelectionNotify. GetAtomName, GetWindowAttributes and | |||
| 69 | + | XFIXES DestroyRegion on bad IDs yield BadAtom, BadWindow and BadRegion. Each | |||
| 70 | + | batch is fenced by a GetInputFocus reply; unexpected packets fail capture. | |||
| 71 | + | `KeymapNotify` and `GeGeneric` (XGE framing with a tail) are hand-written | |||
| 72 | + | (`# hand:`): Xvfb produces neither for these clients. | |||
M internal/xtest/capture_test.go +453 -18
| @@ -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" | |
| @@ -21,11 +24,77 @@ import ( | |||
| 21 | 24 | "golang.org/x/sys/unix" | |
| 22 | 25 | ) | |
| 23 | 26 | ||
| 24 | - | var update = flag.Bool("update", false, "refresh request goldens using Xvfb and a raw socketpair tee") | |
| 27 | + | var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them") | |
| 25 | 28 | ||
| 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"))...) | |
| 90 | + | } | |
| 91 | + | ||
| 92 | + | // TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By | |
| 93 | + | // default it compares every captured packet with the committed golden and | |
| 94 | + | // fails on a difference; -update rewrites the goldens instead. | |
| 26 | 95 | func TestCaptureRequests(t *testing.T) { | |
| 27 | - | if !*update { | |
| 28 | - | t.Skip("pass -update to capture request goldens") | |
| 96 | + | if _, err := exec.LookPath("Xvfb"); err != nil { | |
| 97 | + | t.Skip("Xvfb not installed") | |
| 29 | 98 | } | |
| 30 | 99 | // displayfd avoids display-number races and tells us when Xvfb is ready. | |
| 31 | 100 | r, w, err := os.Pipe() | |
| @@ -33,7 +102,7 @@ func TestCaptureRequests(t *testing.T) { | |||
| 33 | 102 | t.Fatal(err) | |
| 34 | 103 | } | |
| 35 | 104 | defer r.Close() | |
| 36 | - | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac") | |
| 105 | + | cmd := exec.Command("Xvfb", xvfbArgs()...) | |
| 37 | 106 | cmd.ExtraFiles = []*os.File{w} | |
| 38 | 107 | var log bytes.Buffer | |
| 39 | 108 | cmd.Stderr = &log | |
| @@ -327,6 +396,181 @@ func TestCaptureRequests(t *testing.T) { | |||
| 327 | 396 | keep("xproto/GetPropertyEmpty", packet()) | |
| 328 | 397 | write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator | |
| 329 | 398 | keep("xproto/ListExtensions", packet()) | |
| 399 | + | // Event and error captures. The tee client acts as a window manager on the | |
| 400 | + | // root; a second raw client (direct socket, not teed) triggers the events. | |
| 401 | + | // Requests sent here on the tee use window/minor values the request-golden | |
| 402 | + | // classifier below ignores. | |
| 403 | + | xfixesEvent, xfixesError := ext[10], ext[11] | |
| 404 | + | events := map[string][]byte{} | |
| 405 | + | xerrors := map[string][]byte{} | |
| 406 | + | // collect reads events and errors until the reply fencing them. | |
| 407 | + | collect := func(names map[byte]string) { | |
| 408 | + | t.Helper() | |
| 409 | + | write([]byte{43, 0, 1, 0}) // GetInputFocus | |
| 410 | + | for { | |
| 411 | + | p := packet() | |
| 412 | + | switch { | |
| 413 | + | case p[0] == 1: | |
| 414 | + | return | |
| 415 | + | case p[0] == 0: | |
| 416 | + | name := names[p[1]] | |
| 417 | + | if name == "" { | |
| 418 | + | t.Fatalf("unexpected error %x", p) | |
| 419 | + | } | |
| 420 | + | xerrors[name] = p | |
| 421 | + | default: | |
| 422 | + | name := names[p[0]] | |
| 423 | + | if name == "" { | |
| 424 | + | t.Fatalf("unexpected event %x", p) | |
| 425 | + | } | |
| 426 | + | events[name] = p | |
| 427 | + | } | |
| 428 | + | } | |
| 429 | + | } | |
| 430 | + | const ( | |
| 431 | + | maskSubstructureNotify = 1 << 19 | |
| 432 | + | maskSubstructureRedirect = 1 << 20 | |
| 433 | + | maskPropertyChange = 1 << 22 | |
| 434 | + | atomPrimary, atomSecondary, atomString = 1, 2, 31 | |
| 435 | + | ) | |
| 436 | + | selectRoot := make([]byte, 16) | |
| 437 | + | selectRoot[0], selectRoot[2] = 2, 4 | |
| 438 | + | binary.LittleEndian.PutUint32(selectRoot[4:], root) | |
| 439 | + | binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask | |
| 440 | + | binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange) | |
| 441 | + | write(selectRoot) | |
| 442 | + | selectSel := make([]byte, 16) | |
| 443 | + | selectSel[0], selectSel[1], selectSel[2] = major, 2, 4 | |
| 444 | + | binary.LittleEndian.PutUint32(selectSel[4:], root) | |
| 445 | + | binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary) | |
| 446 | + | binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner | |
| 447 | + | write(selectSel) | |
| 448 | + | collect(nil) | |
| 449 | + | ||
| 450 | + | other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second) | |
| 451 | + | if err != nil { | |
| 452 | + | t.Fatal(err) | |
| 453 | + | } | |
| 454 | + | defer other.Close() | |
| 455 | + | _ = other.SetDeadline(time.Now().Add(5 * time.Second)) | |
| 456 | + | send := func(p []byte) { | |
| 457 | + | t.Helper() | |
| 458 | + | if _, err := other.Write(p); err != nil { | |
| 459 | + | t.Fatal(err) | |
| 460 | + | } | |
| 461 | + | } | |
| 462 | + | recv := func(p []byte) { | |
| 463 | + | t.Helper() | |
| 464 | + | if _, err := io.ReadFull(other, p); err != nil { | |
| 465 | + | t.Fatal(err) | |
| 466 | + | } | |
| 467 | + | } | |
| 468 | + | // sync round-trips the second client; any error fails the capture. | |
| 469 | + | sync := func() { | |
| 470 | + | t.Helper() | |
| 471 | + | send([]byte{43, 0, 1, 0}) | |
| 472 | + | var p [32]byte | |
| 473 | + | recv(p[:]) | |
| 474 | + | if p[0] != 1 { | |
| 475 | + | t.Fatalf("second client: %x", p) | |
| 476 | + | } | |
| 477 | + | } | |
| 478 | + | send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}) | |
| 479 | + | var otherHead [8]byte | |
| 480 | + | recv(otherHead[:]) | |
| 481 | + | if otherHead[0] != 1 { | |
| 482 | + | t.Fatalf("second setup failed: %x", otherHead) | |
| 483 | + | } | |
| 484 | + | otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4) | |
| 485 | + | recv(otherSetup) | |
| 486 | + | client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1 | |
| 487 | + | win := make([]byte, 32) | |
| 488 | + | win[0], win[2] = 1, 8 | |
| 489 | + | binary.LittleEndian.PutUint32(win[4:], client2) | |
| 490 | + | binary.LittleEndian.PutUint32(win[8:], root) | |
| 491 | + | binary.LittleEndian.PutUint16(win[12:], 10) | |
| 492 | + | binary.LittleEndian.PutUint16(win[14:], 20) | |
| 493 | + | binary.LittleEndian.PutUint16(win[16:], 300) | |
| 494 | + | binary.LittleEndian.PutUint16(win[18:], 200) | |
| 495 | + | binary.LittleEndian.PutUint16(win[22:], 1) | |
| 496 | + | send(win) // CreateNotify | |
| 497 | + | mapOther := make([]byte, 8) | |
| 498 | + | mapOther[0], mapOther[2] = 8, 2 | |
| 499 | + | binary.LittleEndian.PutUint32(mapOther[4:], client2) | |
| 500 | + | send(mapOther) // MapRequest (redirected) | |
| 501 | + | cfg := make([]byte, 24) | |
| 502 | + | cfg[0], cfg[2] = 12, 6 | |
| 503 | + | binary.LittleEndian.PutUint32(cfg[4:], client2) | |
| 504 | + | binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width | |
| 505 | + | binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb) | |
| 506 | + | binary.LittleEndian.PutUint32(cfg[16:], 40) | |
| 507 | + | binary.LittleEndian.PutUint32(cfg[20:], 640) | |
| 508 | + | send(cfg) // ConfigureRequest (redirected) | |
| 509 | + | prop := make([]byte, 28) | |
| 510 | + | prop[0], prop[2] = 18, 7 | |
| 511 | + | binary.LittleEndian.PutUint32(prop[4:], root) | |
| 512 | + | binary.LittleEndian.PutUint32(prop[8:], atom) | |
| 513 | + | binary.LittleEndian.PutUint32(prop[12:], atomString) | |
| 514 | + | prop[16] = 8 | |
| 515 | + | binary.LittleEndian.PutUint32(prop[20:], 2) | |
| 516 | + | copy(prop[24:], "hi") | |
| 517 | + | send(prop) // PropertyNotify (NewValue) | |
| 518 | + | msg := make([]byte, 44) | |
| 519 | + | msg[0], msg[2] = 25, 11 | |
| 520 | + | binary.LittleEndian.PutUint32(msg[4:], root) | |
| 521 | + | binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect) | |
| 522 | + | msg[12], msg[13] = 33, 32 // ClientMessage, format 32 | |
| 523 | + | binary.LittleEndian.PutUint32(msg[16:], client2) | |
| 524 | + | binary.LittleEndian.PutUint32(msg[20:], atom) | |
| 525 | + | for i := range 5 { | |
| 526 | + | binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1))) | |
| 527 | + | } | |
| 528 | + | send(msg) // ClientMessage (SendEvent bit set) | |
| 529 | + | owner := make([]byte, 16) | |
| 530 | + | owner[0], owner[2] = 22, 4 | |
| 531 | + | binary.LittleEndian.PutUint32(owner[4:], client2) | |
| 532 | + | binary.LittleEndian.PutUint32(owner[8:], atomPrimary) | |
| 533 | + | send(owner) // xfixes SelectionNotify (SetSelectionOwner) | |
| 534 | + | sync() | |
| 535 | + | collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest", | |
| 536 | + | 28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"}) | |
| 537 | + | // The window manager maps the window itself (MapNotify); the second client | |
| 538 | + | // then unmaps and destroys it. | |
| 539 | + | mapByWM := make([]byte, 8) | |
| 540 | + | mapByWM[0], mapByWM[2] = 8, 2 | |
| 541 | + | binary.LittleEndian.PutUint32(mapByWM[4:], client2) | |
| 542 | + | write(mapByWM) | |
| 543 | + | collect(map[byte]string{19: "xproto/MapNotify"}) | |
| 544 | + | unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0} | |
| 545 | + | binary.LittleEndian.PutUint32(unmap[4:], client2) | |
| 546 | + | send(unmap) | |
| 547 | + | destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0} | |
| 548 | + | binary.LittleEndian.PutUint32(destroy[4:], client2) | |
| 549 | + | send(destroy) | |
| 550 | + | sync() | |
| 551 | + | collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"}) | |
| 552 | + | // Core SelectionNotify: SECONDARY has no owner, so the server replies itself. | |
| 553 | + | convert := make([]byte, 24) | |
| 554 | + | convert[0], convert[2] = 24, 6 | |
| 555 | + | binary.LittleEndian.PutUint32(convert[4:], child) | |
| 556 | + | binary.LittleEndian.PutUint32(convert[8:], atomSecondary) | |
| 557 | + | binary.LittleEndian.PutUint32(convert[12:], atomString) | |
| 558 | + | binary.LittleEndian.PutUint32(convert[16:], atom) | |
| 559 | + | write(convert) | |
| 560 | + | collect(map[byte]string{31: "xproto/SelectionNotify"}) | |
| 561 | + | // Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion). | |
| 562 | + | badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d} | |
| 563 | + | write(badAtom) | |
| 564 | + | badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0} | |
| 565 | + | binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777) | |
| 566 | + | write(badWindow) | |
| 567 | + | badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0} | |
| 568 | + | binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888) | |
| 569 | + | write(badRegion) | |
| 570 | + | collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"}) | |
| 571 | + | if len(events) != 10 || len(xerrors) != 3 { | |
| 572 | + | t.Fatalf("captured %d events, %d errors", len(events), len(xerrors)) | |
| 573 | + | } | |
| 330 | 574 | // Closing the client drains the request direction; wait before inspecting tee. | |
| 331 | 575 | client.Close() | |
| 332 | 576 | select { | |
| @@ -353,13 +597,17 @@ func TestCaptureRequests(t *testing.T) { | |||
| 353 | 597 | captured["xproto/CreateWindow"] = p | |
| 354 | 598 | } | |
| 355 | 599 | case 2: | |
| 356 | - | captured["xproto/ChangeWindowAttributes"] = p | |
| 600 | + | if binary.LittleEndian.Uint32(p[4:]) == child { | |
| 601 | + | captured["xproto/ChangeWindowAttributes"] = p | |
| 602 | + | } | |
| 357 | 603 | case 12: | |
| 358 | 604 | names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"} | |
| 359 | 605 | captured["xproto/"+names[configIndex]] = p | |
| 360 | 606 | configIndex++ | |
| 361 | 607 | case 8: | |
| 362 | - | captured["xproto/MapWindow"] = p | |
| 608 | + | if binary.LittleEndian.Uint32(p[4:]) == root { | |
| 609 | + | captured["xproto/MapWindow"] = p | |
| 610 | + | } | |
| 363 | 611 | case 14: | |
| 364 | 612 | captured["xproto/GetGeometry"] = p | |
| 365 | 613 | case 16: | |
| @@ -373,7 +621,7 @@ func TestCaptureRequests(t *testing.T) { | |||
| 373 | 621 | if p[0] == resMajor && p[1] == 4 { | |
| 374 | 622 | captured["res/QueryClientIds"] = p | |
| 375 | 623 | } | |
| 376 | - | if p[0] == major { | |
| 624 | + | if p[0] == major && p[1] == 0 { | |
| 377 | 625 | captured["xfixes/QueryVersion"] = p | |
| 378 | 626 | } | |
| 379 | 627 | } | |
| @@ -382,24 +630,211 @@ func TestCaptureRequests(t *testing.T) { | |||
| 382 | 630 | if len(captured) != 12 || len(replies) != 11 { | |
| 383 | 631 | t.Fatal("missing captured requests or replies") | |
| 384 | 632 | } | |
| 633 | + | out := map[string][]byte{} | |
| 385 | 634 | for name, p := range replies { | |
| 386 | - | path := filepath.Join("../../testdata/golden", name+".reply.hex") | |
| 387 | - | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |
| 635 | + | out[name+".reply"] = p | |
| 636 | + | } | |
| 637 | + | for name, p := range captured { | |
| 638 | + | out[name+".req"] = p | |
| 639 | + | } | |
| 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) | |
| 657 | + | } | |
| 658 | + | rolesJSON, err := json.MarshalIndent(roles, "", " ") | |
| 659 | + | if err != nil { | |
| 388 | 660 | t.Fatal(err) | |
| 389 | 661 | } | |
| 390 | - | text := "# source: Xvfb reply via raw socketpair tee to an independent reference request\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | |
| 391 | - | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |
| 662 | + | if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil { | |
| 392 | 663 | t.Fatal(err) | |
| 393 | 664 | } | |
| 665 | + | return | |
| 394 | 666 | } | |
| 395 | - | for name, p := range captured { | |
| 396 | - | path := filepath.Join("../../testdata/golden", name+".req.hex") | |
| 397 | - | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |
| 398 | - | t.Fatal(err) | |
| 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]) | |
| 399 | 678 | } | |
| 400 | - | text := "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | |
| 401 | - | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |
| 402 | - | t.Fatal(err) | |
| 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") | |
| 695 | + | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |
| 696 | + | t.Fatal(err) | |
| 697 | + | } | |
| 698 | + | source := sources[name[strings.LastIndexByte(name, '.')+1:]] | |
| 699 | + | text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | |
| 700 | + | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |
| 701 | + | t.Fatal(err) | |
| 702 | + | } | |
| 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") | |
| 403 | 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"]) | |
| 404 | 838 | } | |
| 839 | + | return nil | |
| 405 | 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" | ||
| @@ -21,11 +24,77 @@ import ( | |||||
| 21 | "golang.org/x/sys/unix" | 24 | "golang.org/x/sys/unix" | ||
| 22 | ) | 25 | ) | ||
| 23 | 26 | ||||
| 24 | - | var update = flag.Bool("update", false, "refresh request goldens using Xvfb and a raw socketpair tee") | 27 | + | var update = flag.Bool("update", false, "rewrite wire goldens from Xvfb instead of comparing against them") |
| 25 | 28 | ||||
| 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"))...) | |||
| 90 | + | } | |||
| 91 | + | ||||
| 92 | + | // TestCaptureRequests drives a fresh Xvfb through a raw socketpair tee. By | |||
| 93 | + | // default it compares every captured packet with the committed golden and | |||
| 94 | + | // fails on a difference; -update rewrites the goldens instead. | |||
| 26 | func TestCaptureRequests(t *testing.T) { | 95 | func TestCaptureRequests(t *testing.T) { | ||
| 27 | - | if !*update { | 96 | + | if _, err := exec.LookPath("Xvfb"); err != nil { |
| 28 | - | t.Skip("pass -update to capture request goldens") | 97 | + | t.Skip("Xvfb not installed") |
| 29 | } | 98 | } | ||
| 30 | // displayfd avoids display-number races and tells us when Xvfb is ready. | 99 | // displayfd avoids display-number races and tells us when Xvfb is ready. | ||
| 31 | r, w, err := os.Pipe() | 100 | r, w, err := os.Pipe() | ||
| @@ -33,7 +102,7 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 33 | t.Fatal(err) | 102 | t.Fatal(err) | ||
| 34 | } | 103 | } | ||
| 35 | defer r.Close() | 104 | defer r.Close() | ||
| 36 | - | cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-ac") | 105 | + | cmd := exec.Command("Xvfb", xvfbArgs()...) |
| 37 | cmd.ExtraFiles = []*os.File{w} | 106 | cmd.ExtraFiles = []*os.File{w} | ||
| 38 | var log bytes.Buffer | 107 | var log bytes.Buffer | ||
| 39 | cmd.Stderr = &log | 108 | cmd.Stderr = &log | ||
| @@ -327,6 +396,181 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 327 | keep("xproto/GetPropertyEmpty", packet()) | 396 | keep("xproto/GetPropertyEmpty", packet()) | ||
| 328 | write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator | 397 | write([]byte{99, 0, 1, 0}) // ListExtensions: variable STR iterator | ||
| 329 | keep("xproto/ListExtensions", packet()) | 398 | keep("xproto/ListExtensions", packet()) | ||
| 399 | + | // Event and error captures. The tee client acts as a window manager on the | |||
| 400 | + | // root; a second raw client (direct socket, not teed) triggers the events. | |||
| 401 | + | // Requests sent here on the tee use window/minor values the request-golden | |||
| 402 | + | // classifier below ignores. | |||
| 403 | + | xfixesEvent, xfixesError := ext[10], ext[11] | |||
| 404 | + | events := map[string][]byte{} | |||
| 405 | + | xerrors := map[string][]byte{} | |||
| 406 | + | // collect reads events and errors until the reply fencing them. | |||
| 407 | + | collect := func(names map[byte]string) { | |||
| 408 | + | t.Helper() | |||
| 409 | + | write([]byte{43, 0, 1, 0}) // GetInputFocus | |||
| 410 | + | for { | |||
| 411 | + | p := packet() | |||
| 412 | + | switch { | |||
| 413 | + | case p[0] == 1: | |||
| 414 | + | return | |||
| 415 | + | case p[0] == 0: | |||
| 416 | + | name := names[p[1]] | |||
| 417 | + | if name == "" { | |||
| 418 | + | t.Fatalf("unexpected error %x", p) | |||
| 419 | + | } | |||
| 420 | + | xerrors[name] = p | |||
| 421 | + | default: | |||
| 422 | + | name := names[p[0]] | |||
| 423 | + | if name == "" { | |||
| 424 | + | t.Fatalf("unexpected event %x", p) | |||
| 425 | + | } | |||
| 426 | + | events[name] = p | |||
| 427 | + | } | |||
| 428 | + | } | |||
| 429 | + | } | |||
| 430 | + | const ( | |||
| 431 | + | maskSubstructureNotify = 1 << 19 | |||
| 432 | + | maskSubstructureRedirect = 1 << 20 | |||
| 433 | + | maskPropertyChange = 1 << 22 | |||
| 434 | + | atomPrimary, atomSecondary, atomString = 1, 2, 31 | |||
| 435 | + | ) | |||
| 436 | + | selectRoot := make([]byte, 16) | |||
| 437 | + | selectRoot[0], selectRoot[2] = 2, 4 | |||
| 438 | + | binary.LittleEndian.PutUint32(selectRoot[4:], root) | |||
| 439 | + | binary.LittleEndian.PutUint32(selectRoot[8:], 1<<11) // CWEventMask | |||
| 440 | + | binary.LittleEndian.PutUint32(selectRoot[12:], maskSubstructureNotify|maskSubstructureRedirect|maskPropertyChange) | |||
| 441 | + | write(selectRoot) | |||
| 442 | + | selectSel := make([]byte, 16) | |||
| 443 | + | selectSel[0], selectSel[1], selectSel[2] = major, 2, 4 | |||
| 444 | + | binary.LittleEndian.PutUint32(selectSel[4:], root) | |||
| 445 | + | binary.LittleEndian.PutUint32(selectSel[8:], atomPrimary) | |||
| 446 | + | binary.LittleEndian.PutUint32(selectSel[12:], 1) // SetSelectionOwner | |||
| 447 | + | write(selectSel) | |||
| 448 | + | collect(nil) | |||
| 449 | + | ||||
| 450 | + | other, err := net.DialTimeout("unix", fmt.Sprintf("/tmp/.X11-unix/X%d", num), 5*time.Second) | |||
| 451 | + | if err != nil { | |||
| 452 | + | t.Fatal(err) | |||
| 453 | + | } | |||
| 454 | + | defer other.Close() | |||
| 455 | + | _ = other.SetDeadline(time.Now().Add(5 * time.Second)) | |||
| 456 | + | send := func(p []byte) { | |||
| 457 | + | t.Helper() | |||
| 458 | + | if _, err := other.Write(p); err != nil { | |||
| 459 | + | t.Fatal(err) | |||
| 460 | + | } | |||
| 461 | + | } | |||
| 462 | + | recv := func(p []byte) { | |||
| 463 | + | t.Helper() | |||
| 464 | + | if _, err := io.ReadFull(other, p); err != nil { | |||
| 465 | + | t.Fatal(err) | |||
| 466 | + | } | |||
| 467 | + | } | |||
| 468 | + | // sync round-trips the second client; any error fails the capture. | |||
| 469 | + | sync := func() { | |||
| 470 | + | t.Helper() | |||
| 471 | + | send([]byte{43, 0, 1, 0}) | |||
| 472 | + | var p [32]byte | |||
| 473 | + | recv(p[:]) | |||
| 474 | + | if p[0] != 1 { | |||
| 475 | + | t.Fatalf("second client: %x", p) | |||
| 476 | + | } | |||
| 477 | + | } | |||
| 478 | + | send([]byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}) | |||
| 479 | + | var otherHead [8]byte | |||
| 480 | + | recv(otherHead[:]) | |||
| 481 | + | if otherHead[0] != 1 { | |||
| 482 | + | t.Fatalf("second setup failed: %x", otherHead) | |||
| 483 | + | } | |||
| 484 | + | otherSetup := make([]byte, int(binary.LittleEndian.Uint16(otherHead[6:]))*4) | |||
| 485 | + | recv(otherSetup) | |||
| 486 | + | client2 := binary.LittleEndian.Uint32(otherSetup[4:]) | 1 | |||
| 487 | + | win := make([]byte, 32) | |||
| 488 | + | win[0], win[2] = 1, 8 | |||
| 489 | + | binary.LittleEndian.PutUint32(win[4:], client2) | |||
| 490 | + | binary.LittleEndian.PutUint32(win[8:], root) | |||
| 491 | + | binary.LittleEndian.PutUint16(win[12:], 10) | |||
| 492 | + | binary.LittleEndian.PutUint16(win[14:], 20) | |||
| 493 | + | binary.LittleEndian.PutUint16(win[16:], 300) | |||
| 494 | + | binary.LittleEndian.PutUint16(win[18:], 200) | |||
| 495 | + | binary.LittleEndian.PutUint16(win[22:], 1) | |||
| 496 | + | send(win) // CreateNotify | |||
| 497 | + | mapOther := make([]byte, 8) | |||
| 498 | + | mapOther[0], mapOther[2] = 8, 2 | |||
| 499 | + | binary.LittleEndian.PutUint32(mapOther[4:], client2) | |||
| 500 | + | send(mapOther) // MapRequest (redirected) | |||
| 501 | + | cfg := make([]byte, 24) | |||
| 502 | + | cfg[0], cfg[2] = 12, 6 | |||
| 503 | + | binary.LittleEndian.PutUint32(cfg[4:], client2) | |||
| 504 | + | binary.LittleEndian.PutUint16(cfg[8:], 1|2|4) // x, y, width | |||
| 505 | + | binary.LittleEndian.PutUint32(cfg[12:], 0xfffffffb) | |||
| 506 | + | binary.LittleEndian.PutUint32(cfg[16:], 40) | |||
| 507 | + | binary.LittleEndian.PutUint32(cfg[20:], 640) | |||
| 508 | + | send(cfg) // ConfigureRequest (redirected) | |||
| 509 | + | prop := make([]byte, 28) | |||
| 510 | + | prop[0], prop[2] = 18, 7 | |||
| 511 | + | binary.LittleEndian.PutUint32(prop[4:], root) | |||
| 512 | + | binary.LittleEndian.PutUint32(prop[8:], atom) | |||
| 513 | + | binary.LittleEndian.PutUint32(prop[12:], atomString) | |||
| 514 | + | prop[16] = 8 | |||
| 515 | + | binary.LittleEndian.PutUint32(prop[20:], 2) | |||
| 516 | + | copy(prop[24:], "hi") | |||
| 517 | + | send(prop) // PropertyNotify (NewValue) | |||
| 518 | + | msg := make([]byte, 44) | |||
| 519 | + | msg[0], msg[2] = 25, 11 | |||
| 520 | + | binary.LittleEndian.PutUint32(msg[4:], root) | |||
| 521 | + | binary.LittleEndian.PutUint32(msg[8:], maskSubstructureNotify|maskSubstructureRedirect) | |||
| 522 | + | msg[12], msg[13] = 33, 32 // ClientMessage, format 32 | |||
| 523 | + | binary.LittleEndian.PutUint32(msg[16:], client2) | |||
| 524 | + | binary.LittleEndian.PutUint32(msg[20:], atom) | |||
| 525 | + | for i := range 5 { | |||
| 526 | + | binary.LittleEndian.PutUint32(msg[24+4*i:], uint32(0x01020304*(i+1))) | |||
| 527 | + | } | |||
| 528 | + | send(msg) // ClientMessage (SendEvent bit set) | |||
| 529 | + | owner := make([]byte, 16) | |||
| 530 | + | owner[0], owner[2] = 22, 4 | |||
| 531 | + | binary.LittleEndian.PutUint32(owner[4:], client2) | |||
| 532 | + | binary.LittleEndian.PutUint32(owner[8:], atomPrimary) | |||
| 533 | + | send(owner) // xfixes SelectionNotify (SetSelectionOwner) | |||
| 534 | + | sync() | |||
| 535 | + | collect(map[byte]string{16: "xproto/CreateNotify", 20: "xproto/MapRequest", 23: "xproto/ConfigureRequest", | |||
| 536 | + | 28: "xproto/PropertyNotify", 0x80 | 33: "xproto/ClientMessage", xfixesEvent: "xfixes/SelectionNotify"}) | |||
| 537 | + | // The window manager maps the window itself (MapNotify); the second client | |||
| 538 | + | // then unmaps and destroys it. | |||
| 539 | + | mapByWM := make([]byte, 8) | |||
| 540 | + | mapByWM[0], mapByWM[2] = 8, 2 | |||
| 541 | + | binary.LittleEndian.PutUint32(mapByWM[4:], client2) | |||
| 542 | + | write(mapByWM) | |||
| 543 | + | collect(map[byte]string{19: "xproto/MapNotify"}) | |||
| 544 | + | unmap := []byte{10, 0, 2, 0, 0, 0, 0, 0} | |||
| 545 | + | binary.LittleEndian.PutUint32(unmap[4:], client2) | |||
| 546 | + | send(unmap) | |||
| 547 | + | destroy := []byte{4, 0, 2, 0, 0, 0, 0, 0} | |||
| 548 | + | binary.LittleEndian.PutUint32(destroy[4:], client2) | |||
| 549 | + | send(destroy) | |||
| 550 | + | sync() | |||
| 551 | + | collect(map[byte]string{18: "xproto/UnmapNotify", 17: "xproto/DestroyNotify"}) | |||
| 552 | + | // Core SelectionNotify: SECONDARY has no owner, so the server replies itself. | |||
| 553 | + | convert := make([]byte, 24) | |||
| 554 | + | convert[0], convert[2] = 24, 6 | |||
| 555 | + | binary.LittleEndian.PutUint32(convert[4:], child) | |||
| 556 | + | binary.LittleEndian.PutUint32(convert[8:], atomSecondary) | |||
| 557 | + | binary.LittleEndian.PutUint32(convert[12:], atomString) | |||
| 558 | + | binary.LittleEndian.PutUint32(convert[16:], atom) | |||
| 559 | + | write(convert) | |||
| 560 | + | collect(map[byte]string{31: "xproto/SelectionNotify"}) | |||
| 561 | + | // Errors: BadAtom, BadWindow and an extension error (XFIXES BadRegion). | |||
| 562 | + | badAtom := []byte{17, 0, 2, 0, 0xef, 0xbe, 0xad, 0x0d} | |||
| 563 | + | write(badAtom) | |||
| 564 | + | badWindow := []byte{3, 0, 2, 0, 0, 0, 0, 0} | |||
| 565 | + | binary.LittleEndian.PutUint32(badWindow[4:], base|0x7777) | |||
| 566 | + | write(badWindow) | |||
| 567 | + | badRegion := []byte{major, 10, 2, 0, 0, 0, 0, 0} | |||
| 568 | + | binary.LittleEndian.PutUint32(badRegion[4:], base|0x8888) | |||
| 569 | + | write(badRegion) | |||
| 570 | + | collect(map[byte]string{5: "xproto/BadAtom", 3: "xproto/BadWindow", xfixesError: "xfixes/BadRegion"}) | |||
| 571 | + | if len(events) != 10 || len(xerrors) != 3 { | |||
| 572 | + | t.Fatalf("captured %d events, %d errors", len(events), len(xerrors)) | |||
| 573 | + | } | |||
| 330 | // Closing the client drains the request direction; wait before inspecting tee. | 574 | // Closing the client drains the request direction; wait before inspecting tee. | ||
| 331 | client.Close() | 575 | client.Close() | ||
| 332 | select { | 576 | select { | ||
| @@ -353,13 +597,17 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 353 | captured["xproto/CreateWindow"] = p | 597 | captured["xproto/CreateWindow"] = p | ||
| 354 | } | 598 | } | ||
| 355 | case 2: | 599 | case 2: | ||
| 356 | - | captured["xproto/ChangeWindowAttributes"] = p | 600 | + | if binary.LittleEndian.Uint32(p[4:]) == child { |
| 601 | + | captured["xproto/ChangeWindowAttributes"] = p | |||
| 602 | + | } | |||
| 357 | case 12: | 603 | case 12: | ||
| 358 | names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"} | 604 | names := []string{"ConfigureWindow", "ConfigureWindowSparse", "ConfigureWindowAll"} | ||
| 359 | captured["xproto/"+names[configIndex]] = p | 605 | captured["xproto/"+names[configIndex]] = p | ||
| 360 | configIndex++ | 606 | configIndex++ | ||
| 361 | case 8: | 607 | case 8: | ||
| 362 | - | captured["xproto/MapWindow"] = p | 608 | + | if binary.LittleEndian.Uint32(p[4:]) == root { |
| 609 | + | captured["xproto/MapWindow"] = p | |||
| 610 | + | } | |||
| 363 | case 14: | 611 | case 14: | ||
| 364 | captured["xproto/GetGeometry"] = p | 612 | captured["xproto/GetGeometry"] = p | ||
| 365 | case 16: | 613 | case 16: | ||
| @@ -373,7 +621,7 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 373 | if p[0] == resMajor && p[1] == 4 { | 621 | if p[0] == resMajor && p[1] == 4 { | ||
| 374 | captured["res/QueryClientIds"] = p | 622 | captured["res/QueryClientIds"] = p | ||
| 375 | } | 623 | } | ||
| 376 | - | if p[0] == major { | 624 | + | if p[0] == major && p[1] == 0 { |
| 377 | captured["xfixes/QueryVersion"] = p | 625 | captured["xfixes/QueryVersion"] = p | ||
| 378 | } | 626 | } | ||
| 379 | } | 627 | } | ||
| @@ -382,24 +630,211 @@ func TestCaptureRequests(t *testing.T) { | |||||
| 382 | if len(captured) != 12 || len(replies) != 11 { | 630 | if len(captured) != 12 || len(replies) != 11 { | ||
| 383 | t.Fatal("missing captured requests or replies") | 631 | t.Fatal("missing captured requests or replies") | ||
| 384 | } | 632 | } | ||
| 633 | + | out := map[string][]byte{} | |||
| 385 | for name, p := range replies { | 634 | for name, p := range replies { | ||
| 386 | - | path := filepath.Join("../../testdata/golden", name+".reply.hex") | 635 | + | out[name+".reply"] = p |
| 387 | - | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | 636 | + | } |
| 637 | + | for name, p := range captured { | |||
| 638 | + | out[name+".req"] = p | |||
| 639 | + | } | |||
| 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) | |||
| 657 | + | } | |||
| 658 | + | rolesJSON, err := json.MarshalIndent(roles, "", " ") | |||
| 659 | + | if err != nil { | |||
| 388 | t.Fatal(err) | 660 | t.Fatal(err) | ||
| 389 | } | 661 | } | ||
| 390 | - | text := "# source: Xvfb reply via raw socketpair tee to an independent reference request\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | 662 | + | if err := os.WriteFile(rolesPath, append(rolesJSON, '\n'), 0644); err != nil { |
| 391 | - | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |||
| 392 | t.Fatal(err) | 663 | t.Fatal(err) | ||
| 393 | } | 664 | } | ||
| 665 | + | return | |||
| 394 | } | 666 | } | ||
| 395 | - | for name, p := range captured { | 667 | + | rolesJSON, err := os.ReadFile(rolesPath) |
| 396 | - | path := filepath.Join("../../testdata/golden", name+".req.hex") | 668 | + | if err != nil { |
| 397 | - | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | 669 | + | t.Fatal(err) |
| 398 | - | 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]) | |||
| 399 | } | 678 | } | ||
| 400 | - | text := "# source: Xvfb via raw socketpair tee; independent reference encoder; server accepted\n# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | 679 | + | } |
| 401 | - | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | 680 | + | for name, p := range out { |
| 402 | - | t.Fatal(err) | 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") | |||
| 695 | + | if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { | |||
| 696 | + | t.Fatal(err) | |||
| 697 | + | } | |||
| 698 | + | source := sources[name[strings.LastIndexByte(name, '.')+1:]] | |||
| 699 | + | text := source + "# xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace\n" + hex.EncodeToString(p) + "\n" | |||
| 700 | + | if err := os.WriteFile(path, []byte(text), 0644); err != nil { | |||
| 701 | + | t.Fatal(err) | |||
| 702 | + | } | |||
| 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") | |||
| 403 | } | 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"]) | |||
| 404 | } | 838 | } | ||
| 839 | + | return nil | |||
| 405 | } | 840 | } | ||
A render/errors_generated.go +21 -0
| @@ -0,0 +1,21 @@ | |||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |
| 2 | + | package render | |
| 3 | + | ||
| 4 | + | // Error codes: error codes relative to the extension's FirstError (XError.Rel). | |
| 5 | + | const ( | |
| 6 | + | BadPictFormat uint8 = 0 | |
| 7 | + | BadPicture uint8 = 1 | |
| 8 | + | BadPictOp uint8 = 2 | |
| 9 | + | BadGlyphSet uint8 = 3 | |
| 10 | + | BadGlyph uint8 = 4 | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | var errorNames = [...]string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} | |
| 14 | + | ||
| 15 | + | // ErrorName returns the name of an error code (error codes relative to the extension's FirstError (XError.Rel)), or "" when unknown. | |
| 16 | + | func ErrorName(code uint8) string { | |
| 17 | + | if int(code) < len(errorNames) { | |
| 18 | + | return errorNames[code] | |
| 19 | + | } | |
| 20 | + | return "" | |
| 21 | + | } | |
| @@ -0,0 +1,21 @@ | |||||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |||
| 2 | + | package render | |||
| 3 | + | ||||
| 4 | + | // Error codes: error codes relative to the extension's FirstError (XError.Rel). | |||
| 5 | + | const ( | |||
| 6 | + | BadPictFormat uint8 = 0 | |||
| 7 | + | BadPicture uint8 = 1 | |||
| 8 | + | BadPictOp uint8 = 2 | |||
| 9 | + | BadGlyphSet uint8 = 3 | |||
| 10 | + | BadGlyph uint8 = 4 | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | var errorNames = [...]string{"BadPictFormat", "BadPicture", "BadPictOp", "BadGlyphSet", "BadGlyph"} | |||
| 14 | + | ||||
| 15 | + | // ErrorName returns the name of an error code (error codes relative to the extension's FirstError (XError.Rel)), or "" when unknown. | |||
| 16 | + | func ErrorName(code uint8) string { | |||
| 17 | + | if int(code) < len(errorNames) { | |||
| 18 | + | return errorNames[code] | |||
| 19 | + | } | |||
| 20 | + | return "" | |||
| 21 | + | } | |||
A shape/events_fuzz_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |
| 2 | + | package shape | |
| 3 | + | ||
| 4 | + | import "testing" | |
| 5 | + | import "bnema.dev/xturbo" | |
| 6 | + | ||
| 7 | + | func FuzzEventViews(f *testing.F) { | |
| 8 | + | f.Add(make([]byte, 32)) | |
| 9 | + | f.Fuzz(func(t *testing.T, raw []byte) { | |
| 10 | + | if len(raw) < 32 { | |
| 11 | + | return | |
| 12 | + | } | |
| 13 | + | var ev xturbo.Event | |
| 14 | + | copy(ev.Raw[:], raw) | |
| 15 | + | ev.Tail = raw[32:] | |
| 16 | + | { | |
| 17 | + | vNotify := NotifyOf(&ev) | |
| 18 | + | _ = vNotify.ShapeKind() | |
| 19 | + | _ = vNotify.AffectedWindow() | |
| 20 | + | _ = vNotify.ExtentsX() | |
| 21 | + | _ = vNotify.ExtentsY() | |
| 22 | + | _ = vNotify.ExtentsWidth() | |
| 23 | + | _ = vNotify.ExtentsHeight() | |
| 24 | + | _ = vNotify.ServerTime() | |
| 25 | + | _ = vNotify.Shaped() | |
| 26 | + | } | |
| 27 | + | }) | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |||
| 2 | + | package shape | |||
| 3 | + | ||||
| 4 | + | import "testing" | |||
| 5 | + | import "bnema.dev/xturbo" | |||
| 6 | + | ||||
| 7 | + | func FuzzEventViews(f *testing.F) { | |||
| 8 | + | f.Add(make([]byte, 32)) | |||
| 9 | + | f.Fuzz(func(t *testing.T, raw []byte) { | |||
| 10 | + | if len(raw) < 32 { | |||
| 11 | + | return | |||
| 12 | + | } | |||
| 13 | + | var ev xturbo.Event | |||
| 14 | + | copy(ev.Raw[:], raw) | |||
| 15 | + | ev.Tail = raw[32:] | |||
| 16 | + | { | |||
| 17 | + | vNotify := NotifyOf(&ev) | |||
| 18 | + | _ = vNotify.ShapeKind() | |||
| 19 | + | _ = vNotify.AffectedWindow() | |||
| 20 | + | _ = vNotify.ExtentsX() | |||
| 21 | + | _ = vNotify.ExtentsY() | |||
| 22 | + | _ = vNotify.ExtentsWidth() | |||
| 23 | + | _ = vNotify.ExtentsHeight() | |||
| 24 | + | _ = vNotify.ServerTime() | |||
| 25 | + | _ = vNotify.Shaped() | |||
| 26 | + | } | |||
| 27 | + | }) | |||
| 28 | + | } | |||
A shape/events_generated.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |
| 2 | + | package shape | |
| 3 | + | ||
| 4 | + | import "bnema.dev/xturbo/xproto" | |
| 5 | + | import "encoding/binary" | |
| 6 | + | import "bnema.dev/xturbo" | |
| 7 | + | ||
| 8 | + | // KindNotify identifies Notify events in xturbo.Event.Kind. | |
| 9 | + | const KindNotify = xturbo.EventKind(xturbo.ExtShape) << 8 | |
| 10 | + | ||
| 11 | + | // NotifyEvent is a view over a Notify event's 32 bytes. Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from. | |
| 12 | + | type NotifyEvent struct{ raw *[32]byte } | |
| 13 | + | ||
| 14 | + | // NotifyOf views ev as Notify without checking ev.Kind: a wrong kind yields garbage, never a panic. | |
| 15 | + | func NotifyOf(ev *xturbo.Event) NotifyEvent { return NotifyEvent{raw: &ev.Raw} } | |
| 16 | + | func (e NotifyEvent) Raw() *[32]byte { return e.raw } | |
| 17 | + | func (e NotifyEvent) ShapeKind() Sk { return Sk(e.raw[1:][0]) } | |
| 18 | + | func (e NotifyEvent) AffectedWindow() xproto.Window { | |
| 19 | + | return xproto.Window(binary.LittleEndian.Uint32(e.raw[4:])) | |
| 20 | + | } | |
| 21 | + | func (e NotifyEvent) ExtentsX() int16 { return int16(binary.LittleEndian.Uint16(e.raw[8:])) } | |
| 22 | + | func (e NotifyEvent) ExtentsY() int16 { return int16(binary.LittleEndian.Uint16(e.raw[10:])) } | |
| 23 | + | func (e NotifyEvent) ExtentsWidth() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[12:])) } | |
| 24 | + | func (e NotifyEvent) ExtentsHeight() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[14:])) } | |
| 25 | + | func (e NotifyEvent) ServerTime() xproto.Timestamp { | |
| 26 | + | return xproto.Timestamp(binary.LittleEndian.Uint32(e.raw[16:])) | |
| 27 | + | } | |
| 28 | + | func (e NotifyEvent) Shaped() bool { return (e.raw[20:][0] != 0) } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | // Code generated by xtgen; DO NOT EDIT. | |||
| 2 | + | package shape | |||
| 3 | + | ||||
| 4 | + | import "bnema.dev/xturbo/xproto" | |||
| 5 | + | import "encoding/binary" | |||
| 6 | + | import "bnema.dev/xturbo" | |||
| 7 | + | ||||
| 8 | + | // KindNotify identifies Notify events in xturbo.Event.Kind. | |||
| 9 | + | const KindNotify = xturbo.EventKind(xturbo.ExtShape) << 8 | |||
| 10 | + | ||||
| 11 | + | // NotifyEvent is a view over a Notify event's 32 bytes. Getters read the copied Event.Raw, so a view stays valid as long as the Event value it was taken from. | |||
| 12 | + | type NotifyEvent struct{ raw *[32]byte } | |||
| 13 | + | ||||
| 14 | + | // NotifyOf views ev as Notify without checking ev.Kind: a wrong kind yields garbage, never a panic. | |||
| 15 | + | func NotifyOf(ev *xturbo.Event) NotifyEvent { return NotifyEvent{raw: &ev.Raw} } | |||
| 16 | + | func (e NotifyEvent) Raw() *[32]byte { return e.raw } | |||
| 17 | + | func (e NotifyEvent) ShapeKind() Sk { return Sk(e.raw[1:][0]) } | |||
| 18 | + | func (e NotifyEvent) AffectedWindow() xproto.Window { | |||
| 19 | + | return xproto.Window(binary.LittleEndian.Uint32(e.raw[4:])) | |||
| 20 | + | } | |||
| 21 | + | func (e NotifyEvent) ExtentsX() int16 { return int16(binary.LittleEndian.Uint16(e.raw[8:])) } | |||
| 22 | + | func (e NotifyEvent) ExtentsY() int16 { return int16(binary.LittleEndian.Uint16(e.raw[10:])) } | |||
| 23 | + | func (e NotifyEvent) ExtentsWidth() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[12:])) } | |||
| 24 | + | func (e NotifyEvent) ExtentsHeight() uint16 { return uint16(binary.LittleEndian.Uint16(e.raw[14:])) } | |||
| 25 | + | func (e NotifyEvent) ServerTime() xproto.Timestamp { | |||
| 26 | + | return xproto.Timestamp(binary.LittleEndian.Uint32(e.raw[16:])) | |||
| 27 | + | } | |||
| 28 | + | func (e NotifyEvent) Shaped() bool { return (e.raw[20:][0] != 0) } | |||
A testdata/golden/xfixes/BadRegion.error.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | 008c2400888820000a008a000000000000000000000000000000000000000000 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | 008c2400888820000a008a000000000000000000000000000000000000000000 | |||
A testdata/golden/xfixes/SelectionNotify.event.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | 57001b009f03000001004000010000003e8987003e8987000000000000000000 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | 57001b009f03000001004000010000003e8987003e8987000000000000000000 | |||
A testdata/golden/xproto/BadAtom.error.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | 00052200efbead0d000011000000000000000000000000000000000000000000 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | 00052200efbead0d000011000000000000000000000000000000000000000000 | |||
A testdata/golden/xproto/BadWindow.error.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | 0003230077772000000003000000000000000000000000000000000000000000 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb error via raw socketpair tee to an independent reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | 0003230077772000000003000000000000000000000000000000000000000000 | |||
A testdata/golden/xproto/ClientMessage.event.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | a1201b0001004000e700000004030201080604020c090603100c0804140f0a05 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | a1201b0001004000e700000004030201080604020c090603100c0804140f0a05 | |||
A testdata/golden/xproto/ConfigureRequest.event.hex +3 -0
| @@ -0,0 +1,3 @@ | |||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |
| 3 | + | 17001b009f0300000100400000000000fbff28008002c8000000070000000000 | |
| @@ -0,0 +1,3 @@ | |||||
| 1 | + | # source: Xvfb event via raw socketpair tee; triggered by an independent second client or reference request | |||
| 2 | + | # xtrace fallback: local xtrace is glibc's execution tracer, not X11 xtrace | |||
| 3 | + | 17001b009f0300000100400000000000fbff28008002c8000000070000000000 | |||
A testdata/golden/xproto/CreateNotify.event.hex +3 -0
A testdata/golden/xproto/DestroyNotify.event.hex +3 -0
A testdata/golden/xproto/GeGeneric.event.hex +5 -0
A testdata/golden/xproto/KeymapNotify.event.hex +3 -0
A testdata/golden/xproto/MapNotify.event.hex +3 -0
A testdata/golden/xproto/MapRequest.event.hex +3 -0
A testdata/golden/xproto/PropertyNotify.event.hex +3 -0
A testdata/golden/xproto/SelectionNotify.event.hex +3 -0
A testdata/golden/xproto/UnmapNotify.event.hex +3 -0
A testdata/golden/xvfb-roles.json +19 -0
A xerror.go +54 -0
A xerror_test.go +67 -0
A xfixes/errors_generated.go +17 -0
A xfixes/events_fuzz_test.go +40 -0
A xfixes/events_generated.go +57 -0
A xproto/errors_generated.go +33 -0
A xproto/events_fuzz_test.go +1189 -0
A xproto/events_generated.go +1257 -0
Comments0 open / 0
no comments yet.