1package wayland
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/go-wayland-bindings/server/extidlenotify"
9 "github.com/bnema/go-wayland-bindings/server/idleinhibit"
10 "github.com/bnema/go-wayland-bindings/server/wlroutputpowermanagement"
11 "github.com/bnema/neferwl/internal/adapters/logging"
12 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13 "github.com/bnema/neferwl/internal/ports"
14 "github.com/bnema/wlturbo"
15 "github.com/stretchr/testify/mock"
16)
17
18// eventProxy records the opcodes, and first uint argument, of its events.
19type eventProxy struct {
20 wlturbo.BaseProxy
21 events chan [2]uint32
22}
23
24func (p *eventProxy) Dispatch(e *wlturbo.Event) {
25 var arg uint32
26 if len(e.Data()) >= 4 {
27 arg = e.Uint32()
28 }
29 p.events <- [2]uint32{uint32(e.Opcode), arg}
30}
31
32func newEventProxy(c *wlturbo.Display) (uint32, *eventProxy) {
33 id := c.AllocateID()
34 p := &eventProxy{events: make(chan [2]uint32, 16)}
35 p.SetID(id)
36 registerWireProxy(c, p)
37 return id, p
38}
39
40// next dispatches until p gets an event, or fails after wait.
41func (p *eventProxy) next(t *testing.T, c *wlturbo.Display, wait time.Duration) [2]uint32 {
42 t.Helper()
43 deadline := time.Now().Add(wait)
44 for time.Now().Before(deadline) {
45 if err := c.Roundtrip(); err != nil {
46 t.Fatal(err)
47 }
48 select {
49 case e := <-p.events:
50 return e
51 case <-time.After(5 * time.Millisecond):
52 }
53 }
54 t.Fatal("no event")
55 return [2]uint32{}
56}
57
58// none checks p gets no event for d.
59func (p *eventProxy) none(t *testing.T, c *wlturbo.Display, d time.Duration) {
60 t.Helper()
61 deadline := time.Now().Add(d)
62 for time.Now().Before(deadline) {
63 if err := c.Roundtrip(); err != nil {
64 t.Fatal(err)
65 }
66 select {
67 case e := <-p.events:
68 t.Fatalf("unexpected event %v", e)
69 case <-time.After(5 * time.Millisecond):
70 }
71 }
72}
73
74// A notification idles after its timeout, resumes on user activity and
75// idles again; an idle inhibitor holds it, an input-idle one ignores it.
76func TestIdleNotification(t *testing.T) {
77 s, events, commands, _, dir := contentServer(t)
78 c := protocolClient(t, s, dir)
79 seat := bindProtocol(t, c, "wl_seat")
80 registerProtocol(t, c, seat)
81 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)
82 registerProtocol(t, c, notifier)
83 id, n := newEventProxy(c)
84 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(50), seat)
85 idled, resumed := uint32(extidlenotify.ExtIdleNotificationV1EventIdled), uint32(extidlenotify.ExtIdleNotificationV1EventResumed)
86 if e := n.next(t, c, 2*time.Second); e[0] != idled {
87 t.Fatalf("event %v, want idled", e)
88 }
89 commands <- ports.UserActivity{}
90 if e := n.next(t, c, 2*time.Second); e[0] != resumed {
91 t.Fatalf("event %v, want resumed", e)
92 }
93 if e := n.next(t, c, 2*time.Second); e[0] != idled {
94 t.Fatalf("event %v, want idled again", e)
95 }
96 commands <- ports.UserActivity{}
97 n.next(t, c, 2*time.Second) // resumed
98
99 // An idle inhibitor holds it; input-idle notifications still fire.
100 _, surf, _ := surfaceMapper(t, c, events)()
101 inhibitMgr := bindProtocol(t, c, "zwp_idle_inhibit_manager_v1")
102 in := c.AllocateID()
103 registerProtocol(t, c, in)
104 requestProtocol(t, c, inhibitMgr, idleinhibit.ZwpIdleInhibitManagerV1RequestCreateInhibitor, in, surf)
105 clientEvent[ports.IdleInhibit](t, events)
106 inputID, input := newEventProxy(c)
107 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetInputIdleNotification, inputID, uint32(50), seat)
108 if e := input.next(t, c, 2*time.Second); e[0] != idled {
109 t.Fatalf("input-idle event %v, want idled", e)
110 }
111 n.none(t, c, 200*time.Millisecond)
112 // The inhibitor goes: the held notification counts from zero.
113 requestProtocol(t, c, in, idleinhibit.ZwpIdleInhibitorV1RequestDestroy)
114 if e := n.next(t, c, 2*time.Second); e[0] != idled {
115 t.Fatalf("event %v, want idled after the inhibitor", e)
116 }
117 requestProtocol(t, c, id, extidlenotify.ExtIdleNotificationV1RequestDestroy)
118 requestProtocol(t, c, inputID, extidlenotify.ExtIdleNotificationV1RequestDestroy)
119 if err := c.Roundtrip(); err != nil {
120 t.Fatal(err)
121 }
122}
123
124// A D-Bus idle inhibition (IdleInhibited) holds notifications like an
125// inhibitor surface: a running timer stops, input-idle ones still fire,
126// and held ones count from zero once it ends. D-Bus activity
127// (IdleActivity) resumes them like input.
128func TestBusIdleInhibition(t *testing.T) {
129 dir := t.TempDir()
130 events := make(chan ports.ClientEvent, 16)
131 inhibited := make(chan bool)
132 activity := make(chan struct{})
133 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs}, Channels{Events: events, IdleInhibited: inhibited, IdleActivity: activity}, logging.For(context.Background(), "wayland"))
134 if err != nil {
135 t.Fatal(err)
136 }
137 ctx, cancel := context.WithCancel(context.Background())
138 done := make(chan error, 1)
139 go func() { done <- s.Run(ctx) }()
140 t.Cleanup(func() { cancel(); <-done })
141 c := protocolClient(t, s, dir)
142 seat := bindProtocol(t, c, "wl_seat")
143 registerProtocol(t, c, seat)
144 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)
145 registerProtocol(t, c, notifier)
146 idled, resumed := uint32(extidlenotify.ExtIdleNotificationV1EventIdled), uint32(extidlenotify.ExtIdleNotificationV1EventResumed)
147
148 // D-Bus activity resumes an idle notification and restarts it.
149 id, n := newEventProxy(c)
150 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(300), seat)
151 if e := n.next(t, c, 2*time.Second); e[0] != idled {
152 t.Fatalf("event %v, want idled", e)
153 }
154 activity <- struct{}{}
155 if e := n.next(t, c, 2*time.Second); e[0] != resumed {
156 t.Fatalf("event %v, want resumed", e)
157 }
158
159 // Its timer runs again: the inhibition stops it before it fires. The
160 // forwarder takes the second send only once the first is applied.
161 inhibited <- true
162 inhibited <- true
163 n.none(t, c, 500*time.Millisecond)
164 inputID, input := newEventProxy(c)
165 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetInputIdleNotification, inputID, uint32(50), seat)
166 if e := input.next(t, c, 2*time.Second); e[0] != idled {
167 t.Fatalf("input-idle event %v, want idled", e)
168 }
169 n.none(t, c, 100*time.Millisecond)
170 inhibited <- false
171 if e := n.next(t, c, 2*time.Second); e[0] != idled {
172 t.Fatalf("event %v, want idled after the inhibition", e)
173 }
174}
175
176// An output power object reports the mode, turns set_mode into a core
177// event, hears core's decision, and fails when its output goes.
178func TestOutputPower(t *testing.T) {
179 s, events, commands, _, dir := contentServer(t)
180 c := protocolClient(t, s, dir)
181 output := bindProtocol(t, c, "wl_output")
182 registerProtocol(t, c, output)
183 mgr := bindProtocol(t, c, "zwlr_output_power_manager_v1")
184 registerProtocol(t, c, mgr)
185 id, p := newEventProxy(c)
186 requestProtocol(t, c, mgr, wlroutputpowermanagement.ZwlrOutputPowerManagerV1RequestGetOutputPower, id, output)
187 modeEv, failed := uint32(wlroutputpowermanagement.ZwlrOutputPowerV1EventMode), uint32(wlroutputpowermanagement.ZwlrOutputPowerV1EventFailed)
188 on, off := uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOn), uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ModeOff)
189 if e := p.next(t, c, 2*time.Second); e != [2]uint32{modeEv, on} {
190 t.Fatalf("initial %v", e)
191 }
192 name := testOutputs[0].Info.Name
193 requestProtocol(t, c, id, wlroutputpowermanagement.ZwlrOutputPowerV1RequestSetMode, off)
194 if v := clientEvent[ports.OutputPower](t, events); v != (ports.OutputPower{Output: name, On: false}) {
195 t.Fatal(v)
196 }
197 commands <- ports.SetOutputs{Outputs: testOutputs, Off: []string{name}}
198 if e := p.next(t, c, 2*time.Second); e != [2]uint32{modeEv, off} {
199 t.Fatalf("after off %v", e)
200 }
201 // Unchanged state: no event.
202 commands <- ports.SetOutputs{Outputs: testOutputs, Off: []string{name}}
203 p.none(t, c, 50*time.Millisecond)
204 commands <- ports.SetOutputs{Outputs: testOutputs}
205 if e := p.next(t, c, 2*time.Second); e != [2]uint32{modeEv, on} {
206 t.Fatalf("after on %v", e)
207 }
208 // The output goes: the object fails.
209 commands <- ports.SetOutputs{}
210 if e := p.next(t, c, 2*time.Second); e[0] != failed {
211 t.Fatalf("after unplug %v", e)
212 }
213 // Back with the same name: the failed object controls nothing.
214 commands <- ports.SetOutputs{Outputs: testOutputs}
215 requestProtocol(t, c, id, wlroutputpowermanagement.ZwlrOutputPowerV1RequestSetMode, off)
216 if err := c.Roundtrip(); err != nil {
217 t.Fatal(err)
218 }
219 select {
220 case ev := <-events:
221 if v, ok := ev.(ports.OutputPower); ok {
222 t.Fatalf("failed object sent %+v", v)
223 }
224 case <-time.After(50 * time.Millisecond):
225 }
226}
227
228// armed is one idle timer started through the clock.
229type armed struct {
230 d time.Duration
231 fire func()
232}
233
234// idleClockServer runs a server whose idle timers fire only when the test
235// calls them; every started timer is sent on the returned channel.
236func idleClockServer(t *testing.T) (*Server, chan ports.ClientCommand, string, chan armed) {
237 t.Helper()
238 dir := t.TempDir()
239 commands := make(chan ports.ClientCommand, 16)
240 timers := make(chan armed, 16)
241 clock := portsmocks.NewMockClock(t)
242 clock.EXPECT().AfterFunc(mock.Anything, mock.Anything).RunAndReturn(func(d time.Duration, f func()) ports.Timer {
243 timer := portsmocks.NewMockTimer(t)
244 timer.EXPECT().Stop().Return(true).Maybe()
245 timers <- armed{d, f}
246 return timer
247 }).Maybe()
248 s, err := New(Options{RuntimeDir: dir, Outputs: testOutputs, Clock: clock}, Channels{Commands: commands}, logging.For(context.Background(), "wayland"))
249 if err != nil {
250 t.Fatal(err)
251 }
252 ctx, cancel := context.WithCancel(context.Background())
253 done := make(chan error, 1)
254 go func() { done <- s.Run(ctx) }()
255 t.Cleanup(func() { cancel(); <-done })
256 return s, commands, dir, timers
257}
258
259func nextArmed(t *testing.T, timers <-chan armed) armed {
260 t.Helper()
261 select {
262 case a := <-timers:
263 return a
264 case <-time.After(2 * time.Second):
265 t.Fatal("no idle timer started")
266 return armed{}
267 }
268}
269
270// A timeout shorter than the activity interval still waits for it, so
271// continuous input keeps the notification from idling; a timer replaced
272// by user activity is stale when it fires.
273func TestIdleShortTimeout(t *testing.T) {
274 s, commands, dir, timers := idleClockServer(t)
275 c := protocolClient(t, s, dir)
276 seat := bindProtocol(t, c, "wl_seat")
277 registerProtocol(t, c, seat)
278 notifier := bindVersion(t, c, "ext_idle_notifier_v1", 2)
279 id, n := newEventProxy(c)
280 requestProtocol(t, c, notifier, extidlenotify.ExtIdleNotifierV1RequestGetIdleNotification, id, uint32(0), seat)
281 first := nextArmed(t, timers)
282 if first.d != ports.ActivityInterval {
283 t.Fatalf("timer %v, want the activity interval", first.d)
284 }
285 first.fire()
286 if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
287 t.Fatalf("event %v, want idled", e)
288 }
289 commands <- ports.UserActivity{}
290 if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventResumed) {
291 t.Fatalf("event %v, want resumed", e)
292 }
293 stale := nextArmed(t, timers)
294 commands <- ports.UserActivity{}
295 current := nextArmed(t, timers)
296 stale.fire()
297 n.none(t, c, 50*time.Millisecond)
298 current.fire()
299 if e := n.next(t, c, 2*time.Second); e[0] != uint32(extidlenotify.ExtIdleNotificationV1EventIdled) {
300 t.Fatalf("event %v, want idled", e)
301 }
302}
303
304// An unknown mode is a protocol error.
305func TestOutputPowerInvalidMode(t *testing.T) {
306 s, _, _, _, dir := contentServer(t)
307 c := protocolClient(t, s, dir)
308 output := bindProtocol(t, c, "wl_output")
309 registerProtocol(t, c, output)
310 mgr := bindProtocol(t, c, "zwlr_output_power_manager_v1")
311 id := c.AllocateID()
312 registerProtocol(t, c, id)
313 requestProtocol(t, c, mgr, wlroutputpowermanagement.ZwlrOutputPowerManagerV1RequestGetOutputPower, id, output)
314 requestProtocol(t, c, id, wlroutputpowermanagement.ZwlrOutputPowerV1RequestSetMode, uint32(7))
315 expectProtocolError(t, c, id, uint32(wlroutputpowermanagement.ZwlrOutputPowerV1ErrorInvalidMode))
316}