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