1package core_test
2
3import (
4 "context"
5 "sync/atomic"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferwl/internal/core"
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11 "github.com/bnema/neferwl/internal/ports"
12)
13
14// gestureRig is a rig with window 1 mapped on the left output and the
15// pointer on it. The right output is empty: the pointer there has no focus.
16func gestureRig(t *testing.T) *multiRig {
17 t.Helper()
18 r := startMulti(t, nil, left, right)
19 r.mapWindow(t, 1)
20 r.input <- ports.PointerMotion{X: 50, Y: 50}
21 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
22 t.Fatal(v)
23 }
24 if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
25 t.Fatal(v)
26 }
27 return r
28}
29
30// isPointerCommand reports whether a command is one the gesture tests follow:
31// the pointer's and the gestures'.
32func isPointerCommand(cmd ports.ClientCommand) bool {
33 switch cmd.(type) {
34 case ports.PointerFocus, ports.PointerMotionTo, ports.PointerAxisTo, ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
35 return true
36 }
37 return false
38}
39
40// pointerCommand receives the next pointer or gesture command, skipping
41// the configures and focus changes between them.
42func pointerCommand(t *testing.T, ch <-chan ports.ClientCommand) ports.ClientCommand {
43 t.Helper()
44 for {
45 if v := command(t, ch); isPointerCommand(v) {
46 return v
47 }
48 }
49}
50
51// idle fails the test when core sent a command meanwhile. A marker input
52// goes first, so the check waits for core to have handled what came before.
53func (r *multiRig) idle(t *testing.T) {
54 t.Helper()
55 r.input <- ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}
56 if v := pointerCommand(t, r.commands); v != (ports.PointerAxisTo{ID: 1, Axis: ports.PointerAxis{Vertical: ports.ScrollAxis{Set: true, Value: 1}}}) {
57 t.Fatalf("unexpected command %#v", v)
58 }
59}
60
61func TestPinchGoesToPointerFocus(t *testing.T) {
62 r := gestureRig(t)
63 r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
64 r.input <- ports.PinchUpdate{DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second}
65 r.input <- ports.PinchEnd{Time: 3 * time.Second}
66 for _, want := range []ports.ClientCommand{
67 ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
68 ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, DX: 1, DY: -2, Scale: 1.25, Rotation: 5, Time: 2 * time.Second},
69 ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Time: 3 * time.Second},
70 } {
71 if v := pointerCommand(t, r.commands); v != want {
72 t.Fatalf("got %#v, want %#v", v, want)
73 }
74 }
75}
76
77// A removed touchpad ends its gesture with no time: the client gets the
78// last one, so its clock never runs backwards.
79func TestGestureEndWithoutTime(t *testing.T) {
80 r := gestureRig(t)
81 r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
82 r.input <- ports.PinchUpdate{Scale: 1, Time: 2 * time.Second}
83 r.input <- ports.PinchEnd{Cancelled: true}
84 pointerCommand(t, r.commands)
85 pointerCommand(t, r.commands)
86 if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
87 t.Fatal(v)
88 }
89}
90
91func TestHoldGoesToPointerFocus(t *testing.T) {
92 r := gestureRig(t)
93 r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
94 r.input <- ports.HoldEnd{Cancelled: true, Time: 2 * time.Second}
95 if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
96 t.Fatal(v)
97 }
98 if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: 2 * time.Second}) {
99 t.Fatal(v)
100 }
101}
102
103// Swipes of other finger counts than three and four go to the client; the
104// compositor keeps those.
105func TestSwipeFingerCounts(t *testing.T) {
106 for _, fingers := range []int{2, 5} {
107 r := gestureRig(t)
108 r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
109 // The client gets the accelerated deltas.
110 r.input <- ports.SwipeUpdate{DX: 3, DY: 4, AccelDX: 6, AccelDY: 8, Time: 2 * time.Second}
111 r.input <- ports.SwipeEnd{Time: 3 * time.Second}
112 for _, want := range []ports.ClientCommand{
113 ports.GestureBeginTo{ID: 1, Kind: ports.GestureSwipe, Fingers: fingers, Time: time.Second},
114 ports.GestureUpdateTo{ID: 1, Kind: ports.GestureSwipe, DX: 6, DY: 8, Time: 2 * time.Second},
115 ports.GestureEndTo{ID: 1, Kind: ports.GestureSwipe, Time: 3 * time.Second},
116 } {
117 if v := pointerCommand(t, r.commands); v != want {
118 t.Fatalf("%d fingers: got %#v, want %#v", fingers, v, want)
119 }
120 }
121 }
122 for _, fingers := range []int{3, 4} {
123 r := gestureRig(t)
124 r.input <- ports.SwipeBegin{Fingers: fingers, Time: time.Second}
125 r.input <- ports.SwipeUpdate{DX: 40, DY: 0, Time: 2 * time.Second}
126 r.input <- ports.SwipeEnd{Time: 3 * time.Second}
127 r.idle(t)
128 }
129}
130
131// A gesture that began with no window under the pointer reaches nobody,
132// even when the pointer enters a window meanwhile.
133func TestGestureWithoutPointerFocus(t *testing.T) {
134 r := gestureRig(t)
135 r.input <- ports.PointerMotion{X: 300, Y: 50}
136 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
137 t.Fatal(v)
138 }
139 r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
140 r.input <- ports.PointerMotion{X: 50, Y: 50}
141 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
142 t.Fatal(v)
143 }
144 if v := pointerCommand(t, r.commands); v != (ports.PointerMotionTo{ID: 1, X: 50, Y: 50}) {
145 t.Fatal(v)
146 }
147 r.input <- ports.PinchUpdate{Scale: 2, Time: 2 * time.Second}
148 r.input <- ports.PinchEnd{Time: 3 * time.Second}
149 r.idle(t)
150}
151
152// The window losing the pointer focus gets its gesture cancelled before the
153// leave, and nothing of the rest.
154func TestGestureCancelledOnFocusChange(t *testing.T) {
155 r := gestureRig(t)
156 r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
157 if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second}) {
158 t.Fatal(v)
159 }
160 r.input <- ports.PinchUpdate{Scale: 1.1, Time: 2 * time.Second}
161 if v := pointerCommand(t, r.commands); v != (ports.GestureUpdateTo{ID: 1, Kind: ports.GesturePinch, Scale: 1.1, Time: 2 * time.Second}) {
162 t.Fatal(v)
163 }
164 r.input <- ports.PointerMotion{X: 300, Y: 50}
165 if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second}) {
166 t.Fatal(v)
167 }
168 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
169 t.Fatal(v)
170 }
171 r.input <- ports.PinchUpdate{Scale: 1.2, Time: 3 * time.Second}
172 r.input <- ports.PinchEnd{Time: 4 * time.Second}
173 r.input <- ports.PointerMotion{X: 50, Y: 50}
174 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{ID: 1, X: 50, Y: 50}) {
175 t.Fatal(v)
176 }
177}
178
179// A window that goes away mid-gesture has its gesture cancelled with the
180// pointer focus.
181func TestGestureCancelledOnUnmap(t *testing.T) {
182 r := gestureRig(t)
183 r.input <- ports.HoldBegin{Fingers: 3, Time: time.Second}
184 if v := pointerCommand(t, r.commands); v != (ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: time.Second}) {
185 t.Fatal(v)
186 }
187 r.client <- ports.WindowUnmapped{ID: 1}
188 if v := pointerCommand(t, r.commands); v != (ports.GestureEndTo{ID: 1, Kind: ports.GestureHold, Cancelled: true, Time: time.Second}) {
189 t.Fatal(v)
190 }
191 if v := pointerCommand(t, r.commands); v != (ports.PointerFocus{}) {
192 t.Fatal(v)
193 }
194}
195
196// A gesture whose end was lost ends cancelled when the next one begins.
197func TestGestureBeginEndsPrevious(t *testing.T) {
198 r := gestureRig(t)
199 r.input <- ports.PinchBegin{Fingers: 2, Time: time.Second}
200 r.input <- ports.HoldBegin{Fingers: 3, Time: 2 * time.Second}
201 for _, want := range []ports.ClientCommand{
202 ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
203 ports.GestureEndTo{ID: 1, Kind: ports.GesturePinch, Cancelled: true, Time: 2 * time.Second},
204 ports.GestureBeginTo{ID: 1, Kind: ports.GestureHold, Fingers: 3, Time: 2 * time.Second},
205 } {
206 if v := pointerCommand(t, r.commands); v != want {
207 t.Fatalf("got %#v, want %#v", v, want)
208 }
209 }
210}
211
212// A protected session forwards no gesture to a window; one running when
213// the session locks ends with the pointer focus.
214func TestGesturesRefusedWhileLocked(t *testing.T) {
215 var epoch atomic.Value
216 epoch.Store(ports.SecurityState{})
217 gate := portsmocks.NewMockSessionSecurity(t)
218 gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return epoch.Load().(ports.SecurityState) })
219 r := &multiRig{
220 client: make(chan ports.ClientEvent, 16), input: make(chan ports.InputEvent, 32),
221 output: make(chan ports.OutputEvent, 4), reload: make(chan ports.ConfigChanged, 4),
222 commands: make(chan ports.ClientCommand, 256), scenes: make(chan []ports.Scene, 1),
223 }
224 cfg := scrollDefaults()
225 c, err := core.New(cfg, core.Channels{Client: r.client, Input: r.input, Output: r.output, Config: r.reload, Commands: r.commands, Scenes: r.scenes, Constraints: make(chan ports.PointerConstraint, 1)}, core.Options{Security: gate})
226 if err != nil {
227 t.Fatal(err)
228 }
229 ctx, cancel := context.WithCancel(context.Background())
230 done := make(chan error, 1)
231 go func() { done <- c.Run(ctx) }()
232 t.Cleanup(func() { cancel(); <-done })
233 r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 300, Height: 200}}
234 scene(t, r.scenes)
235 r.client <- ports.WindowMapped{ID: 1}
236 scene(t, r.scenes)
237 unlocked := ports.SecurityState{}
238 r.input <- ports.SecurityInput{State: unlocked, Event: ports.PointerMotion{X: 50, Y: 50}}
239 r.input <- ports.SecurityInput{State: unlocked, Event: ports.PinchBegin{Fingers: 2, Time: time.Second}}
240 for _, want := range []ports.ClientCommand{
241 ports.PointerFocus{ID: 1, X: 50, Y: 50},
242 ports.PointerMotionTo{ID: 1, X: 50, Y: 50},
243 ports.GestureBeginTo{ID: 1, Kind: ports.GesturePinch, Fingers: 2, Time: time.Second},
244 } {
245 if v := unwrap(t, r.commands, unlocked); v != want {
246 t.Fatalf("got %#v, want %#v", v, want)
247 }
248 }
249
250 // The lock clears the pointer focus, which cancels the gesture in wayland.
251 locked := ports.SecurityState{Generation: 1, Protected: true}
252 epoch.Store(locked)
253 r.client <- ports.SessionLockChanged{State: locked, Surfaces: []ports.LockSurfacePlacement{{ID: 10, Output: "A", Width: 300, Height: 200}}}
254 sceneMatch(t, r.scenes, func(s ports.Scene) bool { return s.Security == locked && len(s.Windows) == 1 })
255 r.input <- ports.SecurityInput{State: locked, Event: ports.PointerMotion{X: 50, Y: 50}}
256 r.input <- ports.SecurityInput{State: locked, Event: ports.PinchBegin{Fingers: 2, Time: 2 * time.Second}}
257 r.input <- ports.SecurityInput{State: locked, Event: ports.PinchUpdate{Scale: 2, Time: 3 * time.Second}}
258 r.input <- ports.SecurityInput{State: locked, Event: ports.PinchEnd{Time: 4 * time.Second}}
259 r.input <- ports.SecurityInput{State: locked, Event: ports.HoldBegin{Fingers: 3, Time: 5 * time.Second}}
260 r.input <- ports.SecurityInput{State: locked, Event: ports.HoldEnd{Time: 6 * time.Second}}
261 r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeBegin{Fingers: 2, Time: 7 * time.Second}}
262 r.input <- ports.SecurityInput{State: locked, Event: ports.SwipeEnd{Time: 8 * time.Second}}
263 r.input <- ports.SecurityInput{State: locked, Event: ports.KeyEvent{Keysym: "marker", Keycode: 30, Pressed: true}}
264 for {
265 wrapped, ok := receive(t, r.commands).(ports.SecurityCommand)
266 if !ok {
267 t.Fatal("unwrapped command")
268 }
269 switch v := wrapped.Command.(type) {
270 case ports.GestureBeginTo, ports.GestureUpdateTo, ports.GestureEndTo:
271 if wrapped.State == locked {
272 t.Fatalf("gesture forwarded while locked: %#v", v)
273 }
274 case ports.ForwardKey:
275 if wrapped.State == locked {
276 return
277 }
278 }
279 }
280}
281
282// unwrap returns the next pointer command of an envelope of the given epoch.
283func unwrap(t *testing.T, ch <-chan ports.ClientCommand, state ports.SecurityState) ports.ClientCommand {
284 t.Helper()
285 for {
286 wrapped, ok := receive(t, ch).(ports.SecurityCommand)
287 if !ok {
288 t.Fatal("unwrapped command")
289 }
290 if wrapped.State != state {
291 t.Fatalf("epoch %+v, want %+v", wrapped.State, state)
292 }
293 if isPointerCommand(wrapped.Command) {
294 return wrapped.Command
295 }
296 }
297}