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