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