1package core_test
2
3import (
4 "context"
5 "errors"
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 "github.com/stretchr/testify/mock"
14)
15
16// steppingClock returns a clock that moves a second forward on each Now:
17// a fullscreen request is never within fullscreenGrace of the mapping.
18func steppingClock(t *testing.T) *portsmocks.MockClock {
19 t.Helper()
20 now := time.Unix(0, 0)
21 clock := portsmocks.NewMockClock(t)
22 clock.EXPECT().Now().RunAndReturn(func() time.Time {
23 now = now.Add(time.Second)
24 return now
25 }).Maybe()
26 // Timers (the focus pulse) never fire: tests drive events only.
27 clock.EXPECT().NewTimer(mock.Anything).RunAndReturn(func(time.Duration) ports.Timer {
28 timer := portsmocks.NewMockTimer(t)
29 timer.EXPECT().C().Return(make(chan time.Time)).Maybe()
30 timer.EXPECT().Stop().Return(true).Maybe()
31 timer.EXPECT().Reset(mock.Anything).Return(false).Maybe()
32 return timer
33 }).Maybe()
34 return clock
35}
36
37// persistClock returns a clock whose debounce timer fires when the test
38// sends on the returned channel. Each change restarts the timer.
39func persistClock(t *testing.T, debounce time.Duration, changes int) (*portsmocks.MockClock, chan time.Time) {
40 t.Helper()
41 fire := make(chan time.Time)
42 timer := portsmocks.NewMockTimer(t)
43 timer.EXPECT().Stop().Return(true)
44 timer.EXPECT().Reset(debounce).Return(false).Times(changes)
45 timer.EXPECT().C().Return(fire)
46 clock := portsmocks.NewMockClock(t)
47 clock.EXPECT().NewTimer(debounce).Return(timer).Once()
48 return clock, fire
49}
50
51// A burst of scale changes saves only the last scale of each output, once.
52func TestPersistScalesDebounces(t *testing.T) {
53 store := portsmocks.NewMockOutputScaleStore(t)
54 notes := portsmocks.NewMockNotifier(t)
55 store.EXPECT().SaveOutputScale("DP-1", 1.25).Return(nil).Once()
56 store.EXPECT().SaveOutputScale("DP-2", 2.0).Return(nil).Once()
57 notes.EXPECT().Notify("DP-1 scale saved", "Scale 1.25 is kept after restart").Once()
58 notes.EXPECT().Notify("DP-2 scale saved", "Scale 2 is kept after restart").Once()
59 clock, fire := persistClock(t, time.Second, 4)
60 changes := make(chan ports.ScaleChanged)
61 ctx, cancel := context.WithCancel(context.Background())
62 stopped := make(chan struct{})
63 go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
64 for _, ev := range []ports.ScaleChanged{{Output: "DP-2", Scale: 1.5}, {Output: "DP-1", Scale: 1.25}, {Output: "DP-2", Scale: 2}} {
65 changes <- ev
66 }
67 fire <- time.Time{}
68 // The next change is received only after the fired batch is saved.
69 changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.25}
70 cancel()
71 <-stopped
72}
73
74// A failed save is reported and not retried.
75func TestPersistScalesReportsFailure(t *testing.T) {
76 store := portsmocks.NewMockOutputScaleStore(t)
77 notes := portsmocks.NewMockNotifier(t)
78 store.EXPECT().SaveOutputScale("DP-1", 1.5).Return(errors.New("read-only file system")).Once()
79 notes.EXPECT().Notify("DP-1 scale not saved", "read-only file system").Once()
80 clock, fire := persistClock(t, time.Second, 1)
81 changes := make(chan ports.ScaleChanged)
82 ctx, cancel := context.WithCancel(context.Background())
83 stopped := make(chan struct{})
84 go func() { core.PersistScales(ctx, changes, store, notes, clock, time.Second); close(stopped) }()
85 changes <- ports.ScaleChanged{Output: "DP-1", Scale: 1.5}
86 fire <- time.Time{}
87 cancel()
88 <-stopped
89}
90
91// Scale binds report the new scale of the focused output; a bind at the end
92// of the range changes nothing and reports nothing.
93func TestScaleBindReportsScale(t *testing.T) {
94 cfg := config.Defaults()
95 input := make(chan ports.InputEvent, 8)
96 output := make(chan ports.OutputEvent, 8)
97 scales := make(chan ports.ScaleChanged, 8)
98 scenes := make(chan []ports.Scene, 1)
99 c, err := core.New(cfg, core.Channels{Input: input, Output: output, Commands: make(chan ports.ClientCommand, 64), Scenes: scenes, Scales: scales})
100 if err != nil {
101 t.Fatal(err)
102 }
103 ctx, cancel := context.WithCancel(context.Background())
104 defer cancel()
105 go c.Run(ctx)
106 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "DP-2", Width: 5120, Height: 2160}}
107 scene(t, scenes)
108 press := func(sym string, code uint32) {
109 input <- ports.KeyEvent{Keysym: sym, Keycode: code, Mods: ports.ModSuper, Pressed: true}
110 input <- ports.KeyEvent{Keysym: sym, Keycode: code, Mods: ports.ModSuper}
111 }
112 press("minus", 12) // already at 1
113 press("equal", 13)
114 if got := receive(t, scales); got.Output != "DP-2" || got.Scale <= 1 {
115 t.Fatalf("%+v", got)
116 }
117 if len(scales) != 0 {
118 t.Fatalf("scale-down at 1 reported %+v", <-scales)
119 }
120}