1package indicator
2
3import (
4 "context"
5 "strings"
6 "testing"
7 "time"
8
9 "github.com/bnema/neferclient"
10 "github.com/bnema/wlturbo"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/nefercap/internal/ports"
15)
16
17func TestAppendElapsed(t *testing.T) {
18 for secs, want := range map[int64]string{
19 0: "00:00", 5: "00:05", 65: "01:05", 3599: "59:59", 3600: "1:00:00", 36061: "10:01:01",
20 } {
21 assert.Equal(t, want, string(appendElapsed(nil, secs)), "%d", secs)
22 }
23}
24
25func TestRefreshFollowsTheReadingAndCachesSecond(t *testing.T) {
26 start := time.Unix(100, 0)
27 h := newHUD(start, make(chan struct{}, 1), "")
28 assert.Equal(t, "● REC 00:00", h.refresh(start))
29 assert.Equal(t, "● REC 00:00", h.refresh(start.Add(999*time.Millisecond)))
30 assert.Equal(t, "● REC 01:01", h.refresh(start.Add(61*time.Second)))
31 assert.Equal(t, "● REC 1:00:00", h.refresh(start.Add(time.Hour)))
32 assert.Equal(t, "● REC 00:00", h.refresh(start.Add(-time.Second)), "a reading before start never shows negative time")
33}
34
35func TestTargetLabel(t *testing.T) {
36 start := time.Unix(100, 0)
37 at := start.Add(5 * time.Second)
38 for label, want := range map[string]string{
39 "": "● REC 00:05",
40 "DP-1": "● REC 00:05 · DP-1",
41 "web": "● REC 00:05 · web",
42 "123456789012": "● REC 00:05 · 123456789012",
43 "1234567890123": "● REC 00:05 · 12345678901…",
44 "espace de travail": "● REC 00:05 · espace de t…",
45 "a\nb\x00c": "● REC 00:05 · abc",
46 "\n\t": "● REC 00:05",
47 } {
48 assert.Equal(t, want, newHUD(start, make(chan struct{}, 1), label).refresh(at), "%q", label)
49 }
50 // The longest line still fits the 228 px text area (about 29 cells).
51 long := newHUD(start, make(chan struct{}, 1), "wwwwwwwwwwwwwwwwww").refresh(start.Add(time.Hour))
52 assert.LessOrEqual(t, len([]rune(long)), 29)
53}
54
55func TestTargetLabelIsSingleLinePrintable(t *testing.T) {
56 for name, label := range map[string]string{
57 "line separator": "a\u2028b",
58 "paragraph separator": "a\u2029b",
59 "bidi override": "a\u202eb",
60 "bidi isolate": "a\u2066b\u2069",
61 "bidi mark": "a\u200fb\u061c",
62 "nbsp": "a\u00a0b",
63 "zero width": "a\u200bb",
64 "private use": "a\ue000b",
65 "next line": "a\u0085b",
66 "unassigned": "a\U0010ffffb",
67 } {
68 got := suffixFor(label)
69 assert.Equal(t, " · ab", got, name)
70 }
71 assert.Equal(t, " · café 東京", suffixFor("café 東京"), "printable non-ASCII stays")
72 assert.Equal(t, "", suffixFor("\u202e\u2028"))
73}
74
75func TestSuffixLongInput(t *testing.T) {
76 label := strings.Repeat("\x00界", 100000)
77 assert.Equal(t, " · 界界界界界界界界界界界…", suffixFor(label))
78 assert.Equal(t, " · 123456789012", suffixFor("123456789012"+strings.Repeat("\n", 100000)))
79}
80
81var suffixSink string
82
83func TestSuffixAllocations(t *testing.T) {
84 label := strings.Repeat("\x00界", 100000)
85 allocs := testing.AllocsPerRun(100, func() { suffixSink = suffixFor(label) })
86 t.Logf("long suffix: %.0f allocs/run", allocs)
87 if allocs > 1 {
88 t.Fatalf("suffix allocated %.0f times, want only the returned string", allocs)
89 }
90}
91
92func BenchmarkSuffixFor(b *testing.B) {
93 for name, label := range map[string]string{
94 "short": "workspace",
95 "long": strings.Repeat("\x00界", 100000),
96 } {
97 b.Run(name, func(b *testing.B) {
98 b.ReportAllocs()
99 for b.Loop() {
100 suffixSink = suffixFor(label)
101 }
102 })
103 }
104}
105
106func TestRefreshAllocations(t *testing.T) {
107 if raceEnabled {
108 t.Skip("the race detector adds allocations")
109 }
110 start := time.Unix(100, 0)
111 h := newHUD(start, make(chan struct{}, 1), "workspace-one")
112 at := start.Add(7 * time.Second)
113 h.refresh(at)
114 assert.Equal(t, 0.0, testing.AllocsPerRun(100, func() { h.refresh(at) }))
115}
116
117func TestRequestStopNeverBlocks(t *testing.T) {
118 stop := make(chan struct{}, 1)
119 h := newHUD(time.Now(), stop, "")
120 h.requestStop()
121 h.requestStop() // channel full: the second request is dropped, not blocked
122 assert.Len(t, stop, 1)
123}
124
125func TestLayerConfigTakesNoFocusAndOnlyStopInput(t *testing.T) {
126 c := layerConfig("DP-1", ports.Region{})
127 assert.Equal(t, neferclient.KeyboardNone, c.Keyboard)
128 assert.Equal(t, neferclient.LayerOverlay, c.Level)
129 assert.Equal(t, neferclient.AnchorTop, c.Anchors)
130 assert.Equal(t, [2]int32{Width, Height}, [2]int32{c.Width, c.Height}, "the compositor needs the size on unanchored axes")
131 assert.Equal(t, int32(-1), c.ExclusiveZone)
132 require.Len(t, c.InputRects, 1)
133 r := c.InputRects[0]
134 assert.Equal(t, neferclient.Rect{X: Width - stopWidth, Width: stopWidth, Height: Height}, r)
135 assert.LessOrEqual(t, r.X+r.Width, int32(Width))
136}
137
138func TestLayerConfigCentresOnTheRecordedFrame(t *testing.T) {
139 c := layerConfig("DP-1", ports.Region{X: 100, Y: 50, Width: 1000, Height: 600})
140 assert.Equal(t, neferclient.AnchorTop|neferclient.AnchorLeft, c.Anchors)
141 assert.Equal(t, [4]int32{50 + topMargin, 0, 0, 100 + (1000-Width)/2}, c.Margin)
142 assert.Equal(t, neferclient.KeyboardNone, c.Keyboard)
143 // A frame narrower than the HUD starts at its left edge.
144 c = layerConfig("DP-1", ports.Region{X: 7, Width: 100, Height: 100})
145 assert.Equal(t, int32(7), c.Margin[3])
146 // No frame: the whole output, centred by the compositor.
147 c = layerConfig("DP-1", ports.Region{})
148 assert.Equal(t, neferclient.AnchorTop, c.Anchors)
149 assert.Equal(t, [4]int32{topMargin, 0, 0, 0}, c.Margin)
150}
151
152func TestHUDReadabilityAndStopGeometry(t *testing.T) {
153 assert.Equal(t, 384, Width)
154 assert.Equal(t, 40, Height)
155 assert.Equal(t, 90, stopWidth)
156 assert.Contains(t, styleSheet, "font-size: 1rem")
157 assert.Contains(t, styleSheet, "width: 5.625rem")
158 assert.Contains(t, styleSheet, "line-height: 2.5rem")
159 assert.Contains(t, styleSheet, "padding: 0; line-height:")
160 assert.Equal(t, neferclient.Rect{X: 294, Y: 0, Width: 90, Height: 40}, stopRect())
161}
162
163func BenchmarkRefreshUnchanged(b *testing.B) {
164 start := time.Unix(100, 0)
165 h := newHUD(start, make(chan struct{}, 1), "workspace")
166 h.refresh(start)
167 b.ReportAllocs()
168 for b.Loop() {
169 h.refresh(start)
170 }
171}
172
173func TestRunRejectsBadSetup(t *testing.T) {
174 stop := make(chan struct{}, 1)
175 ok := func(context.Context, *wlturbo.Display, wlturbo.Proxy) error { return nil }
176 out := ports.Output{ID: 1, Name: "DP-1"}
177
178 assert.ErrorIs(t, New(nil, stop).Run(context.Background(), out), ErrNoAuthorize)
179 assert.ErrorIs(t, New(ok, nil).Run(context.Background(), out), ErrNoStop)
180 assert.NotErrorIs(t, New(ok, nil).Run(context.Background(), out), ErrNoAuthorize)
181 assert.ErrorIs(t, New(ok, stop).Run(context.Background(), ports.Output{ID: 1}), ports.ErrOutputNotFound)
182 ctx, cancel := context.WithCancel(context.Background())
183 cancel()
184 assert.ErrorIs(t, New(ok, stop).Run(ctx, out), context.Canceled)
185}