v0.4.0 b6be3a8

idle_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close