1//go:build linux
2
3package client_test
4
5import (
6 "errors"
7 "reflect"
8 "sync"
9 "testing"
10
11 "github.com/bnema/go-wayland-bindings/client/extsessionlock"
12 "github.com/bnema/go-wayland-bindings/client/wayland"
13 "github.com/bnema/wlturbo"
14)
15
16// These tests use ordinary generated proxies and the production transport on a
17// socketpair. Only the server wire peer is scripted; no request/dispatch hooks
18// are replaced, and no live compositor or session lock is used.
19func TestSessionLockLockedLifetime(t *testing.T) {
20 s := newWireEnv(t)
21 ctx := s.d.Context()
22 compositor := wayland.NewCompositor(ctx)
23 s.bind(1, wayland.CompositorInterface, 6, 6, compositor)
24 surface, err := compositor.CreateSurface()
25 noerr(t, err)
26 wantWords(t, "create_surface", s.req(compositor.ID(), 0), surface.ID())
27 out := wayland.NewOutput(ctx)
28 s.bind(2, wayland.OutputInterface, 4, 4, out)
29 mgr := extsessionlock.NewExtSessionLockManager(ctx)
30 s.bind(3, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
31 lock, err := mgr.Lock()
32 noerr(t, err)
33 wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
34 if lock.Version() != 1 || lock.Context() != ctx {
35 t.Fatalf("lock version=%d context=%p", lock.Version(), lock.Context())
36 }
37 noerr(t, ctx.CheckProxy(lock))
38
39 // The manager destructor must not invalidate its lock children.
40 noerr(t, mgr.Destroy())
41 wantWords(t, "manager.destroy", s.req(mgr.ID(), 0))
42 if ctx.CheckProxy(mgr) == nil {
43 t.Fatal("destroyed manager remains registered")
44 }
45 s.event(1, 1, wordBytes(mgr.ID())...) // wl_display.delete_id
46 noerr(t, ctx.CheckProxy(lock))
47 ls, err := lock.GetLockSurface(surface, out)
48 noerr(t, err)
49 wantWords(t, "get_lock_surface", s.req(lock.ID(), 1), ls.ID(), surface.ID(), out.ID())
50 if ls.Version() != 1 || ls.Context() != ctx {
51 t.Fatalf("lock surface version=%d context=%p", ls.Version(), ls.Context())
52 }
53 noerr(t, ctx.CheckProxy(ls))
54
55 var events []string
56 lock.OnLocked(func() { events = append(events, "locked") })
57 lock.OnFinished(func() { events = append(events, "finished") })
58 var configures [][3]uint32
59 ls.OnConfigure(func(serial, width, height uint32) {
60 configures = append(configures, [3]uint32{serial, width, height})
61 noerr(t, ls.AckConfigure(serial))
62 })
63 s.event(ls.ID(), 0, wordBytes(0x89abcdef, 1920, 1080)...)
64 wantWords(t, "ack_configure", s.req(ls.ID(), 1), 0x89abcdef)
65 s.event(lock.ID(), 0)
66 s.event(lock.ID(), 1) // finished after locked still requires unlock_and_destroy
67 if !reflect.DeepEqual(events, []string{"locked", "finished"}) ||
68 !reflect.DeepEqual(configures, [][3]uint32{{0x89abcdef, 1920, 1080}}) {
69 t.Fatalf("events=%v configures=%v", events, configures)
70 }
71 // Neither event is a destructor; the application must choose the request.
72 noerr(t, ctx.CheckProxy(lock))
73 noerr(t, lock.UnlockAndDestroy())
74 wantWords(t, "unlock_and_destroy", s.req(lock.ID(), 2))
75 if ctx.CheckProxy(lock) == nil {
76 t.Fatal("unlocked lock remains registered")
77 }
78 s.event(1, 1, wordBytes(lock.ID())...)
79
80 // Lock destruction also leaves its children and core objects valid.
81 noerr(t, ctx.CheckProxy(ls))
82 noerr(t, ctx.CheckProxy(surface))
83 noerr(t, ctx.CheckProxy(out))
84 s.event(ls.ID(), 0, wordBytes(0xffffffff, 800, 600)...)
85 wantWords(t, "ack after parent destruction", s.req(ls.ID(), 1), 0xffffffff)
86 if len(configures) != 2 || configures[1] != [3]uint32{0xffffffff, 800, 600} {
87 t.Fatal(configures)
88 }
89 noerr(t, ls.Destroy())
90 wantWords(t, "surface.destroy", s.req(ls.ID(), 0))
91 if ctx.CheckProxy(ls) == nil {
92 t.Fatal("destroyed lock surface remains registered")
93 }
94 // Events in flight on a destroyed object are dropped until delete_id.
95 s.event(ls.ID(), 0, wordBytes(12, 640, 480)...)
96 if len(configures) != 2 {
97 t.Fatal("configure delivered to destroyed surface")
98 }
99 s.event(1, 1, wordBytes(ls.ID())...)
100 if err := ls.AckConfigure(12); err == nil {
101 t.Fatal("ack on destroyed surface succeeded")
102 }
103 if err := ls.Destroy(); err == nil {
104 t.Fatal("second surface destructor succeeded")
105 }
106 if err := lock.Destroy(); err == nil {
107 t.Fatal("second lock destructor succeeded")
108 }
109 if child, err := mgr.Lock(); err == nil || child != nil {
110 t.Fatal("destroyed manager created a child", child, err)
111 }
112 if child, err := lock.GetLockSurface(surface, out); err == nil || child != nil {
113 t.Fatal("destroyed lock created a child", child, err)
114 }
115 s.noRequest()
116 noerr(t, surface.Destroy())
117 wantWords(t, "wl_surface.destroy", s.req(surface.ID(), 0))
118 noerr(t, out.Release())
119 wantWords(t, "wl_output.release", s.req(out.ID(), 0))
120}
121
122func TestSessionLockDeniedDestroy(t *testing.T) {
123 s := newWireEnv(t)
124 mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
125 s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
126 lock, err := mgr.Lock()
127 noerr(t, err)
128 wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
129 var locked, finished bool
130 lock.OnLocked(func() { locked = true })
131 lock.OnFinished(func() { finished = true })
132 s.event(lock.ID(), 1)
133 if locked || !finished {
134 t.Fatalf("locked=%t finished=%t", locked, finished)
135 }
136 noerr(t, s.d.Context().CheckProxy(lock))
137 noerr(t, lock.Destroy()) // never locked: destroy, not unlock_and_destroy
138 wantWords(t, "denied.destroy", s.req(lock.ID(), 0))
139 if err := lock.UnlockAndDestroy(); err == nil {
140 t.Fatal("second destructor succeeded")
141 }
142 s.noRequest()
143 // Destroying one child must not invalidate the manager or its next child.
144 noerr(t, s.d.Context().CheckProxy(mgr))
145 next, err := mgr.Lock()
146 noerr(t, err)
147 wantWords(t, "next lock", s.req(mgr.ID(), 1), next.ID())
148 noerr(t, next.Destroy())
149 wantWords(t, "next.destroy", s.req(next.ID(), 0))
150 noerr(t, mgr.Destroy())
151 wantWords(t, "manager.destroy", s.req(mgr.ID(), 0))
152}
153
154func TestSessionLockConcurrentDestructors(t *testing.T) {
155 s := newWireEnv(t)
156 mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
157 s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
158 lock, err := mgr.Lock()
159 noerr(t, err)
160 wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
161 s.event(lock.ID(), 1)
162 var wg sync.WaitGroup
163 results := make(chan error, 8)
164 for range 8 {
165 wg.Add(1)
166 go func() { defer wg.Done(); results <- lock.Destroy() }()
167 }
168 wg.Wait()
169 close(results)
170 successes := 0
171 for err := range results {
172 if err == nil {
173 successes++
174 }
175 }
176 if successes != 1 {
177 t.Fatalf("successful destructors=%d, want 1", successes)
178 }
179 wantWords(t, "single destructor", s.req(lock.ID(), 0))
180 s.noRequest()
181}
182
183func TestSessionLockMalformedEvents(t *testing.T) {
184 for _, tc := range []struct {
185 name string
186 surface bool
187 op uint16
188 body []byte
189 }{
190 {"locked trailing word", false, 0, wordBytes(1)},
191 {"finished trailing word", false, 1, wordBytes(1)},
192 {"configure missing height", true, 0, wordBytes(1, 800)},
193 {"configure trailing word", true, 0, wordBytes(1, 800, 600, 1)},
194 } {
195 t.Run(tc.name, func(t *testing.T) {
196 s := newWireEnv(t)
197 mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
198 s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
199 lock, err := mgr.Lock()
200 noerr(t, err)
201 s.req(mgr.ID(), 1)
202 id := lock.ID()
203 called := false
204 lock.OnLocked(func() { called = true })
205 lock.OnFinished(func() { called = true })
206 if tc.surface {
207 ls, err := lock.GetLockSurface(s.surface(), s.output())
208 noerr(t, err)
209 s.req(lock.ID(), 1)
210 ls.OnConfigure(func(_, _, _ uint32) { called = true })
211 id = ls.ID()
212 }
213 send(t, s.p, payload(id, tc.op, tc.body...))
214 if err := s.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
215 t.Fatalf("malformed event: %v", err)
216 }
217 if called {
218 t.Fatal("malformed event reached a handler")
219 }
220 })
221 }
222}