1package wlturbo
2
3import (
4 "bytes"
5 "encoding/binary"
6 "errors"
7 "io"
8 "net"
9 "os"
10 "testing"
11 "time"
12)
13
14// A read deadline must not leave the Display permanently broken.
15func TestDispatchDeadlineIsNotSticky(t *testing.T) {
16 client, server := net.Pipe()
17 defer client.Close()
18 defer server.Close()
19 d := newDisplay(client)
20 records := []EventRecord{}
21 p := &recordingProxy{BaseProxy: BaseProxy{id: 7, context: d.context}, records: &records}
22 d.objects.Store(uint32(7), p)
23 d.RegisterEventSignature(7, 4, "uint,")
24
25 if err := client.SetReadDeadline(time.Now().Add(20 * time.Millisecond)); err != nil {
26 t.Fatal(err)
27 }
28 if err := d.Dispatch(); !errors.Is(err, os.ErrDeadlineExceeded) {
29 t.Fatalf("Dispatch = %v, want deadline exceeded", err)
30 }
31 if err := client.SetReadDeadline(time.Time{}); err != nil {
32 t.Fatal(err)
33 }
34 go func() { _, _ = server.Write(message(7, 4, []byte{1, 0, 0, 0})) }()
35 if err := d.Dispatch(); err != nil {
36 t.Fatalf("Dispatch after clearing deadline = %v", err)
37 }
38 if len(records) != 1 {
39 t.Fatalf("records = %d, want 1", len(records))
40 }
41}
42
43type countingGlobalHandler struct{ n *int }
44
45func (h countingGlobalHandler) HandleRegistryGlobal(RegistryGlobalEvent) { *h.n++ }
46
47// Every registered global handler runs, not only the last one.
48func TestRegistryGlobalHandlersAccumulate(t *testing.T) {
49 d := newDisplay(&chunkConn{})
50 var a, b, specific int
51 d.registry.AddGlobalHandler(countingGlobalHandler{&a})
52 d.registry.AddGlobalHandler(countingGlobalHandler{&b})
53 d.registry.AddHandler("abc", func(*Registry, uint32, uint32) { specific++ })
54 d.registry.handleGlobal(1, "abc", 1)
55 if a != 1 || b != 1 || specific != 1 {
56 t.Fatalf("calls a=%d b=%d specific=%d, want 1 each", a, b, specific)
57 }
58}
59
60type fdSigProxy struct {
61 BaseProxy
62 calls int
63}
64
65func (p *fdSigProxy) EventSignature(op uint16) (string, bool) {
66 switch op {
67 case 0:
68 return "uint,", true
69 case 1:
70 return "fd,", true
71 }
72 return "", false
73}
74func (p *fdSigProxy) Dispatch(*Event) { p.calls++ }
75
76// Events already in flight for an object the client destroyed are dropped,
77// with their descriptors closed, until the compositor sends delete_id.
78func TestDestroyedObjectEventsAreDroppedUntilDeleteID(t *testing.T) {
79 client, peer := unixSocketPair(t)
80 d := newDisplay(client)
81 p := &fdSigProxy{BaseProxy: BaseProxy{id: d.AllocateID(), context: d.context}}
82 d.context.Register(p)
83 if err := d.context.SendDestructor(p, 0); err != nil {
84 t.Fatal(err)
85 }
86 if err := d.context.SendRequest(p, 1); err == nil {
87 t.Fatal("request on destroyed proxy succeeded")
88 }
89
90 r, w, err := os.Pipe()
91 if err != nil {
92 t.Fatal(err)
93 }
94 defer r.Close()
95 sendMessageWithFDs(t, peer, message(p.ID(), 0, []byte{1, 0, 0, 0}), nil)
96 sendMessageWithFDs(t, peer, message(p.ID(), 1, nil), []int{int(w.Fd())})
97 w.Close()
98 for i := 0; i < 2; i++ {
99 if err := d.Dispatch(); err != nil {
100 t.Fatalf("in-flight event %d: %v", i, err)
101 }
102 }
103 if p.calls != 0 {
104 t.Fatalf("destroyed proxy received %d events", p.calls)
105 }
106 if err := r.SetReadDeadline(time.Now().Add(2 * time.Second)); err != nil {
107 t.Fatal(err)
108 }
109 if _, err := r.Read(make([]byte, 1)); !errors.Is(err, io.EOF) {
110 t.Fatalf("descriptor for destroyed object not closed: %v", err)
111 }
112
113 body := make([]byte, 4)
114 binary.LittleEndian.PutUint32(body, p.ID())
115 sendMessageWithFDs(t, peer, message(displayObjectID, 1, body), nil)
116 if err := d.Dispatch(); err != nil {
117 t.Fatalf("delete_id for zombie: %v", err)
118 }
119 sendMessageWithFDs(t, peer, message(p.ID(), 0, []byte{1, 0, 0, 0}), nil)
120 if err := d.Dispatch(); !errors.Is(err, ErrUnknownObject) {
121 t.Fatalf("event after delete_id = %v, want ErrUnknownObject", err)
122 }
123 if got := d.AllocateID(); got != p.ID() {
124 t.Fatalf("ID %d not reused after delete_id (got %d)", p.ID(), got)
125 }
126}
127
128// A plain Go int is ambiguous (value or descriptor) and must be rejected
129// instead of being written as zero.
130func TestMarshalRejectsPlainInt(t *testing.T) {
131 d := newDisplay(&chunkConn{})
132 if err := d.marshalArg(&bytes.Buffer{}, 42); err == nil {
133 t.Fatal("marshalArg(int) succeeded, want error")
134 }
135 if err := d.SendRequest(9, 0, 42); err == nil {
136 t.Fatal("SendRequest with int argument succeeded, want error")
137 }
138}
139
140func TestCheckVersion(t *testing.T) {
141 if err := CheckVersion(0, 5, "x.y"); err != nil {
142 t.Fatalf("unknown version rejected: %v", err)
143 }
144 if err := CheckVersion(5, 5, "x.y"); err != nil {
145 t.Fatalf("exact version rejected: %v", err)
146 }
147 if err := CheckVersion(4, 5, "x.y"); !errors.Is(err, ErrVersionTooLow) {
148 t.Fatalf("older version = %v, want ErrVersionTooLow", err)
149 }
150}
151
152// Bind records the negotiated version so generated requests can check it.
153func TestBindRecordsVersion(t *testing.T) {
154 d := newDisplay(&chunkConn{})
155 p := &BaseProxy{}
156 if err := d.Registry().Bind(1, "wl_seat", 3, p); err != nil {
157 t.Fatal(err)
158 }
159 if p.Version() != 3 {
160 t.Fatalf("version = %d, want 3", p.Version())
161 }
162}
163
164// A late Unregister of a destroyed proxy (for example from a handler that was
165// already running) must not remove its zombie.
166func TestUnregisterKeepsZombie(t *testing.T) {
167 d := newDisplay(&chunkConn{})
168 p := &fdSigProxy{BaseProxy: BaseProxy{id: d.AllocateID(), context: d.context}}
169 d.context.Register(p)
170 if err := d.context.SendDestructor(p, 0); err != nil {
171 t.Fatal(err)
172 }
173 d.context.Unregister(p)
174 if obj, _ := d.objects.Load(p.ID()); obj == nil {
175 t.Fatal("Unregister removed the zombie")
176 } else if _, ok := obj.(*zombie); !ok {
177 t.Fatalf("object = %T, want *zombie", obj)
178 }
179}
180
181// Context.Request owns the request lifecycle generated code relies on.
182func TestContextRequestLifecycle(t *testing.T) {
183 d := newDisplay(&chunkConn{})
184 parent := &BaseProxy{id: d.AllocateID(), context: d.context, version: 2}
185 d.context.Register(parent)
186
187 child := &BaseProxy{context: d.context}
188 if err := d.context.Request(Request{Proxy: parent, Opcode: 0, Name: "x.make", Child: child}, child); err != nil {
189 t.Fatal(err)
190 }
191 if child.ID() == 0 || child.Version() != 2 {
192 t.Fatalf("child id=%d version=%d, want allocated id and version 2", child.ID(), child.Version())
193 }
194 if _, ok := d.objects.Load(child.ID()); !ok {
195 t.Fatal("child not registered")
196 }
197
198 late := &BaseProxy{context: d.context}
199 err := d.context.Request(Request{Proxy: parent, Opcode: 1, Name: "x.late", Since: 3, Child: late}, late)
200 if !errors.Is(err, ErrVersionTooLow) {
201 t.Fatalf("since=3 on v2 = %v, want ErrVersionTooLow", err)
202 }
203 if late.ID() != 0 {
204 t.Fatal("child allocated for a refused request")
205 }
206
207 bad := &BaseProxy{context: d.context}
208 if err := d.context.Request(Request{Proxy: parent, Opcode: 2, Name: "x.bad", Child: bad}, bad, 42); err == nil {
209 t.Fatal("marshal error not reported")
210 }
211 if _, ok := d.objects.Load(bad.ID()); ok {
212 t.Fatal("child left registered after a failed send")
213 }
214}