1package core_test
2
3import (
4 "context"
5 "slices"
6 "testing"
7 "testing/synctest"
8 "time"
9
10 "github.com/bnema/neferwl/internal/adapters/config"
11 "github.com/bnema/neferwl/internal/core"
12 "github.com/bnema/neferwl/internal/ports"
13)
14
15func powerCore(t *testing.T) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) {
16 t.Helper()
17 return powerCoreWith(t, core.Options{})
18}
19
20func powerCoreWith(t *testing.T, opts core.Options) (chan ports.InputEvent, chan ports.ClientEvent, chan ports.OutputEvent, chan ports.ClientCommand, chan []ports.Scene) {
21 t.Helper()
22 input := make(chan ports.InputEvent, 4)
23 client := make(chan ports.ClientEvent, 4)
24 output := make(chan ports.OutputEvent, 2)
25 commands := make(chan ports.ClientCommand, 64)
26 scenes := make(chan []ports.Scene, 1)
27 c, err := core.New(config.Defaults(), core.Channels{Input: input, Client: client, Output: output, Commands: commands, Scenes: scenes, Spawn: make(chan ports.SpawnRequest, 4)}, opts)
28 if err != nil {
29 t.Fatal(err)
30 }
31 ctx, cancel := context.WithCancel(context.Background())
32 done := make(chan error, 1)
33 go func() { done <- c.Run(ctx) }()
34 t.Cleanup(func() { cancel(); receive(t, done) })
35 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
36 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
37 return input, client, output, commands, scenes
38}
39
40// sceneOf waits for a scene set in which pred holds for the named output.
41func sceneOf(t *testing.T, scenes <-chan []ports.Scene, name string, pred func(ports.Scene) bool) {
42 t.Helper()
43 deadline := time.After(2 * time.Second)
44 for {
45 select {
46 case set := <-scenes:
47 for _, s := range set {
48 if s.Output == name && pred(s) {
49 return
50 }
51 }
52 case <-deadline:
53 t.Fatalf("no matching scene for %s", name)
54 }
55 }
56}
57
58// A client turning an output off marks its scene and tells wayland; back
59// on clears both. Other outputs stay on.
60func TestOutputPower(t *testing.T) {
61 _, client, _, commands, scenes := powerCore(t)
62 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
63 client <- ports.OutputPower{Output: "OUT-2", On: false}
64 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
65 for {
66 if v := commandOf[ports.SetOutputs](t, commands); slices.Equal(v.Off, []string{"OUT-2"}) {
67 break
68 }
69 }
70 client <- ports.OutputPower{Output: "OUT-2", On: true}
71 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
72 for {
73 if v := commandOf[ports.SetOutputs](t, commands); len(v.Off) == 0 {
74 break
75 }
76 }
77 // Unknown outputs are ignored.
78 client <- ports.OutputPower{Output: "NOPE", On: false}
79 sceneOf(t, scenes, "OUT-1", func(s ports.Scene) bool { return !s.Off })
80}
81
82// An output unplugged while off (a display in deep sleep) reconnects off;
83// after input it reconnects on. Alone, it is the last output: core keeps a
84// placeholder screen meanwhile.
85func TestOutputReconnectedWhileOff(t *testing.T) {
86 for _, alone := range []bool{false, true} {
87 t.Run(map[bool]string{false: "beside another", true: "alone"}[alone], func(t *testing.T) {
88 input, client, output, commands, scenes := powerCore(t)
89 stop := make(chan struct{})
90 defer close(stop)
91 active := make(chan struct{}, 1)
92 go func() { // core blocks on a full command channel
93 for {
94 select {
95 case c := <-commands:
96 if _, ok := c.(ports.UserActivity); ok {
97 active <- struct{}{}
98 }
99 case <-stop:
100 return
101 }
102 }
103 }()
104 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
105 if alone {
106 output <- ports.OutputRemoved{Name: "OUT-1"}
107 gone(t, scenes, "OUT-1")
108 }
109 client <- ports.OutputPower{Output: "OUT-2", On: false}
110 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
111 output <- ports.OutputRemoved{Name: "OUT-2"}
112 gone(t, scenes, "OUT-2") // no older scene can answer below
113 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
114 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
115
116 client <- ports.OutputPower{Output: "OUT-2", On: true}
117 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
118 client <- ports.OutputPower{Output: "OUT-2", On: false}
119 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return s.Off })
120 output <- ports.OutputRemoved{Name: "OUT-2"}
121 gone(t, scenes, "OUT-2")
122 input <- ports.PointerMotion{X: 1, Y: 1}
123 select { // input and outputs are separate channels: order them
124 case <-active:
125 case <-time.After(2 * time.Second):
126 t.Fatal("input not handled")
127 }
128 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-2", Width: 100, Height: 80}}
129 sceneOf(t, scenes, "OUT-2", func(s ports.Scene) bool { return !s.Off })
130 })
131 }
132}
133
134// gone waits for a scene set without the named output.
135func gone(t *testing.T, scenes <-chan []ports.Scene, name string) {
136 t.Helper()
137 deadline := time.After(2 * time.Second)
138 for {
139 select {
140 case set := <-scenes:
141 if !slices.ContainsFunc(set, func(s ports.Scene) bool { return s.Output == name }) {
142 return
143 }
144 case <-deadline:
145 t.Fatalf("%s still in the scenes", name)
146 }
147 }
148}
149
150// Input reports user activity to wayland, at most once per interval.
151// The bubble's fake clock makes the interval exact.
152func TestUserActivity(t *testing.T) {
153 synctest.Test(t, func(t *testing.T) {
154 input, _, _, commands, _ := powerCore(t)
155 for range 3 {
156 input <- ports.PointerMotion{X: 1, Y: 1}
157 }
158 commandOf[ports.UserActivity](t, commands)
159 time.Sleep(ports.ActivityInterval / 2)
160 synctest.Wait()
161 for len(commands) > 0 {
162 if _, ok := (<-commands).(ports.UserActivity); ok {
163 t.Fatal("second UserActivity within the interval")
164 }
165 }
166 time.Sleep(ports.ActivityInterval)
167 input <- ports.PointerMotion{X: 2, Y: 2}
168 commandOf[ports.UserActivity](t, commands)
169 })
170}
171
172// With an injected clock, the throttle follows it: no second UserActivity
173// until the clock has advanced past the interval.
174func TestUserActivityFollowsClock(t *testing.T) {
175 clk := newStepClock(t)
176 input, _, _, commands, _ := powerCoreWith(t, core.Options{Clock: clk.clock})
177 input <- ports.PointerMotion{X: 1, Y: 1}
178 commandOf[ports.UserActivity](t, commands)
179 clk.advance(ports.ActivityInterval / 2)
180 // Core takes input in order and the input channel holds 4: once the
181 // sixth send returns, the first motion is handled, and a wrongly
182 // unthrottled activity would already be in commands.
183 for range 6 {
184 input <- ports.PointerMotion{X: 1, Y: 1}
185 }
186 for {
187 select {
188 case c := <-commands:
189 if _, ok := c.(ports.UserActivity); ok {
190 t.Fatal("second UserActivity before the interval passed on the clock")
191 }
192 continue
193 default:
194 }
195 break
196 }
197 clk.advance(ports.ActivityInterval)
198 input <- ports.PointerMotion{X: 2, Y: 2}
199 commandOf[ports.UserActivity](t, commands)
200}