1package core_test
2
3import (
4 "sync"
5 "testing"
6 "time"
7
8 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9 "github.com/bnema/neferwl/internal/ports"
10 "github.com/stretchr/testify/mock"
11)
12
13// stepClock is a clock the test moves: springs sample it, so a test lands
14// them by advancing it and sending page flips, never by sleeping. Its
15// timers never fire: frames come from the test.
16type stepClock struct {
17 mu sync.Mutex
18 now time.Time
19 clock ports.Clock
20}
21
22func newStepClock(t *testing.T) *stepClock {
23 t.Helper()
24 s := &stepClock{now: time.Unix(100, 0)}
25 clock := portsmocks.NewMockClock(t)
26 clock.EXPECT().Now().RunAndReturn(func() time.Time {
27 s.mu.Lock()
28 defer s.mu.Unlock()
29 return s.now
30 }).Maybe()
31 // One timer built before core runs: a mock created by core while the
32 // test ends would race its own cleanup check.
33 timer := portsmocks.NewMockTimer(t)
34 timer.EXPECT().C().Return(make(chan time.Time)).Maybe()
35 timer.EXPECT().Stop().Return(true).Maybe()
36 timer.EXPECT().Reset(mock.Anything).Return(false).Maybe()
37 clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe()
38 s.clock = clock
39 return s
40}
41
42func (s *stepClock) advance(d time.Duration) {
43 s.mu.Lock()
44 s.now = s.now.Add(d)
45 s.mu.Unlock()
46}
47
48// flip moves the clock by d and sends a page flip for each output, then
49// returns the scene set core published for them (ok false: none, so no
50// spring ran). Core publishes before it takes its next event, so a flip that
51// names no output is a barrier: once its send returns, every earlier event
52// is handled and the scene channel holds the result. One is sent first too,
53// and a scene left over from earlier events is dropped, so a scene found
54// afterwards is the flips' own.
55func (s *stepClock) flip(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, d time.Duration, outputs ...string) ([]ports.Scene, bool) {
56 t.Helper()
57 send(frames, ports.OutputFrame{Output: "sync"})
58 select {
59 case <-scenes:
60 default:
61 }
62 s.advance(d)
63 for _, o := range outputs {
64 send(frames, ports.OutputFrame{Output: o})
65 }
66 send(frames, ports.OutputFrame{Output: "sync"})
67 select {
68 case set := <-scenes:
69 return set, true
70 default:
71 return nil, false
72 }
73}
74
75// settle is flip past every spring.
76func (s *stepClock) settle(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, outputs ...string) ([]ports.Scene, bool) {
77 t.Helper()
78 return s.flip(t, frames, scenes, 5*time.Second, outputs...)
79}
80
81// landRig is a multiRig whose settle lands every running spring: with
82// animations on it runs on a clock the test moves (swipeRig) and sends the
83// page flips; with animations off it is the plain rig and land is the
84// identity. Each test body runs once per mode, so the off variant keeps
85// its meaning and the on variant asserts the same settled facts.
86type landRig struct {
87 *multiRig
88 land func([]ports.Scene) []ports.Scene
89 // lands counts the settles that found a spring running.
90 lands *int
91}
92
93// startLanding runs core on the given outputs (gaps and terminal policy as
94// in startMulti) with animations on or off.
95func startLanding(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *landRig {
96 t.Helper()
97 if !animated {
98 r := startMulti(t, func(c *ports.Config) {
99 if edit != nil {
100 edit(c)
101 }
102 c.Animations.On = false
103 }, outs...)
104 return &landRig{multiRig: r, land: func(set []ports.Scene) []ports.Scene { return set }, lands: new(int)}
105 }
106 defaults := altCmdDefaults()
107 r := startSwipeOn(t, func(c *ports.Config) {
108 c.Layout.Gaps, c.Terminal.AutoOpen = defaults.Layout.Gaps, defaults.Terminal.AutoOpen
109 if edit != nil {
110 edit(c)
111 }
112 c.Animations.On = true
113 }, outs...)
114 lands := new(int)
115 // An on variant that never saw a spring would be the off variant again.
116 t.Cleanup(func() {
117 if *lands == 0 {
118 t.Error("no spring ever ran: the animations-on variant checked nothing")
119 }
120 })
121 return &landRig{multiRig: r.multiRig, lands: lands, land: func(set []ports.Scene) []ports.Scene {
122 landed, ok := r.settleAll(t, r.outs...)
123 if !ok {
124 return set
125 }
126 *lands++
127 return landed
128 }}
129}
130
131// both runs body with animations off, then on. A rig of the on run fails the
132// test when it never settled a running spring (startLanding,
133// startAnimatedDragRig).
134func both(t *testing.T, body func(t *testing.T, animated bool)) {
135 t.Helper()
136 t.Run("off", func(t *testing.T) { body(t, false) })
137 t.Run("on", func(t *testing.T) { body(t, true) })
138}
139
140// keyLanded presses a key and returns the settled scene set.
141func (r *landRig) keyLanded(t *testing.T, sym string, mods ports.Mods) []ports.Scene {
142 t.Helper()
143 return r.land(r.key(t, sym, mods))
144}
145
146// mapLanded maps a window and returns the settled scene set.
147func (r *landRig) mapLanded(t *testing.T, id ports.WindowID) []ports.Scene {
148 t.Helper()
149 return r.land(r.mapWindow(t, id))
150}