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