1package core_test
2
3import (
4 "context"
5 "testing"
6 "testing/synctest"
7
8 "github.com/bnema/neferwl/internal/core"
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// outboxRig runs a core whose command channel nobody reads until the test
13// does. Run and the sender live in a synctest bubble, so synctest.Wait
14// returns once both are idle.
15type outboxRig struct {
16 input chan ports.InputEvent
17 client chan ports.ClientEvent
18 output chan ports.OutputEvent
19 commands chan ports.ClientCommand
20 scenes chan []ports.Scene
21}
22
23func startOutboxRig(t *testing.T, commandCap int) *outboxRig {
24 t.Helper()
25 cfg := scrollDefaults()
26 cfg.Border.Width = 0
27 cfg.Outputs = []ports.OutputConfig{{Name: "DP-2", Scale: 2}}
28 cfg.Binds["Cmd+minus"] = "scale-down"
29 r := &outboxRig{
30 input: make(chan ports.InputEvent),
31 client: make(chan ports.ClientEvent),
32 output: make(chan ports.OutputEvent),
33 commands: make(chan ports.ClientCommand, commandCap),
34 scenes: make(chan []ports.Scene, 1),
35 }
36 c, err := core.New(cfg, core.Channels{Input: r.input, Client: r.client, Output: r.output, Commands: r.commands, Scenes: r.scenes}, core.Options{})
37 if err != nil {
38 t.Fatal(err)
39 }
40 ctx, cancel := context.WithCancel(context.Background())
41 done := make(chan error, 1)
42 go func() { done <- c.Run(ctx) }()
43 t.Cleanup(func() {
44 cancel()
45 // Run waits for its sender, so a sender stuck on the full channel
46 // would keep this from returning.
47 if err := receive(t, done); err != nil {
48 t.Error(err)
49 }
50 })
51 r.output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "DP-2", Width: 5120, Height: 2160}}
52 r.client <- ports.WindowMapped{ID: 1}
53 synctest.Wait()
54 // Settle: drop the scenes and the commands of the map.
55 r.drain()
56 return r
57}
58
59// drain empties the scene and command channels and lets the sender refill
60// them from its queue until nothing moves.
61func (r *outboxRig) drain() []ports.ClientCommand {
62 var got []ports.ClientCommand
63 for {
64 synctest.Wait()
65 select {
66 case <-r.scenes:
67 default:
68 }
69 select {
70 case v := <-r.commands:
71 got = append(got, v)
72 default:
73 synctest.Wait()
74 if len(r.commands) == 0 {
75 return got
76 }
77 }
78 }
79}
80
81// A display that reads nothing must not stall core: motion keeps coming, and
82// a bind after it still runs and publishes its scene.
83func TestCommandsNeverBlockCore(t *testing.T) {
84 synctest.Test(t, func(t *testing.T) {
85 r := startOutboxRig(t, 32)
86 const n = 10000
87 for i := range n {
88 // Each send returns only once Run is back in its select.
89 r.input <- ports.PointerMotion{X: 500 + float64(i%100), Y: 200, DX: 1, DY: 2, UnaccelDX: 3, UnaccelDY: 4}
90 }
91 r.input <- ports.KeyEvent{Keysym: "minus", Keycode: 12, Mods: ports.ModSuper, Pressed: true}
92 synctest.Wait()
93 select {
94 case set := <-r.scenes:
95 if set[0].Scale != 5.0/3 {
96 t.Fatalf("scale %v, want 5/3", set[0].Scale)
97 }
98 default:
99 t.Fatal("no scene: core is blocked on commands")
100 }
101 // Once the display reads again, it gets everything in order, with
102 // the motion merged and no movement lost.
103 var dx, dy, ux, uy float64
104 var motions int
105 var scaled bool
106 for _, v := range r.drain() {
107 switch v := v.(type) {
108 case ports.PointerMotionTo:
109 motions++
110 dx, dy, ux, uy = dx+v.DX, dy+v.DY, ux+v.UnaccelDX, uy+v.UnaccelDY
111 case ports.SetOutputs:
112 scaled = v.Outputs[0].Scale == 5.0/3
113 }
114 }
115 if dx != n || dy != 2*n || ux != 3*n || uy != 4*n {
116 t.Fatalf("lost movement: %v %v %v %v", dx, dy, ux, uy)
117 }
118 if motions > 40 {
119 t.Fatalf("%d motions reached the display, want them merged while it was away", motions)
120 }
121 if !scaled {
122 t.Fatal("scale change not sent after the motions")
123 }
124 })
125}
126
127// Commands reach the display in the order core made them. Only adjacent
128// motions of one window merge.
129func TestCommandsKeepOrderAndMergeAdjacentMotion(t *testing.T) {
130 synctest.Test(t, func(t *testing.T) {
131 r := startOutboxRig(t, 0)
132 // No reader: the sender holds the first command, which is in flight
133 // and no longer merges, and the rest queues behind it.
134 motion := func(x, dx float64) {
135 r.input <- ports.PointerMotion{X: x, Y: 200, DX: dx, UnaccelDX: dx}
136 synctest.Wait()
137 }
138 // How the first commands split between the sender's batch and the
139 // queue depends on when it wakes. The first key is a barrier: what
140 // is pushed after the owner handled it never merges with what
141 // came before, so the rest is deterministic.
142 motion(500, 1)
143 r.input <- ports.KeyEvent{Keysym: "y", Keycode: 21, Pressed: true}
144 synctest.Wait()
145 motion(501, 2)
146 motion(502, 4)
147 r.input <- ports.PointerButton{Button: 0x110, Pressed: true}
148 synctest.Wait()
149 motion(503, 8)
150 r.input <- ports.KeyEvent{Keysym: "x", Keycode: 45, Pressed: true}
151 synctest.Wait()
152 motion(504, 16)
153 motion(505, 32)
154 var got []ports.ClientCommand
155 behind := false
156 for _, v := range r.drainUnbuffered() {
157 switch v.(type) {
158 case ports.UserActivity, ports.SetOutputs:
159 continue
160 }
161 if k, ok := v.(ports.ForwardKey); ok && k.Key.Keysym == "y" {
162 behind = true
163 continue
164 }
165 if behind {
166 got = append(got, v)
167 }
168 }
169 want := []ports.ClientCommand{
170 ports.PointerMotionTo{ID: 1, X: 502, Y: 200, DX: 6, UnaccelDX: 6},
171 ports.PointerButtonTo{ID: 1, Button: 0x110, Pressed: true, Grab: true},
172 ports.PointerMotionTo{ID: 1, X: 503, Y: 200, DX: 8, UnaccelDX: 8},
173 ports.ForwardKey{ID: 1, Key: ports.KeyEvent{Keysym: "x", Keycode: 45, Pressed: true}},
174 ports.PointerMotionTo{ID: 1, X: 505, Y: 200, DX: 48, UnaccelDX: 48},
175 }
176 wantOrder(t, got, want)
177 })
178}
179
180// drainUnbuffered receives what the sender hands over one by one.
181func (r *outboxRig) drainUnbuffered() []ports.ClientCommand {
182 var got []ports.ClientCommand
183 for {
184 synctest.Wait()
185 select {
186 case v := <-r.commands:
187 got = append(got, v)
188 default:
189 return got
190 }
191 }
192}
193
194func wantOrder(t *testing.T, got, want []ports.ClientCommand) {
195 t.Helper()
196 if len(got) != len(want) {
197 t.Fatalf("got %d commands, want %d:\n%#v", len(got), len(want), got)
198 }
199 for i := range want {
200 if got[i] != want[i] {
201 t.Errorf("command %d: got %#v, want %#v", i, got[i], want[i])
202 }
203 }
204}