1package ansi
2
3import (
4 "reflect"
5 "testing"
6)
7
8func testFrame(rows ...string) Frame {
9 frame := NewFrame(len([]rune(rows[0])), len(rows))
10 fillFrame(frame, rows)
11 return frame
12}
13
14func TestPlanDelta(t *testing.T) {
15 scrollCommitted := testFrame("0000", "1111", "2222", "3333", "4444", "5555", "6666", "7777")
16 scrolled := testFrame("0000", "2222", "3333", "4444", "5555", "6666", "new!", "7777")
17 largeFrame := NewFrame(1, maxPlannedDamageSpans+1)
18 styledCommitted := NewFrame(10, 1)
19 styledCommitted.Set(3, 0, Cell{Rune: ' ', Style: Style{Bold: true, Foreground: -1, Background: -1}})
20 styledFrame := styledCommitted.Clone()
21 styledFrame.Set(0, 0, Cell{Rune: 'a', Style: DefaultStyle()})
22 styledFrame.Set(9, 0, Cell{Rune: 'z', Style: DefaultStyle()})
23
24 tests := []struct {
25 name string
26 frame Frame
27 damage []Damage
28 committed Frame
29 reset bool
30 want DeltaPlan
31 }{
32 {
33 name: "reset snapshots",
34 frame: NewFrame(4, 2),
35 committed: NewFrame(4, 2),
36 reset: true,
37 want: DeltaPlan{Snapshot: true},
38 },
39 {
40 name: "dimension mismatch snapshots",
41 frame: NewFrame(4, 2),
42 committed: NewFrame(3, 2),
43 want: DeltaPlan{Snapshot: true},
44 },
45 {
46 name: "unchanged same size has no spans",
47 frame: NewFrame(4, 2),
48 committed: NewFrame(4, 2),
49 want: DeltaPlan{},
50 },
51 {
52 name: "unchanged full redraw has no spans",
53 frame: NewFrame(4, 2),
54 damage: []Damage{FullRedraw()},
55 committed: NewFrame(4, 2),
56 want: DeltaPlan{},
57 },
58 {
59 name: "changed row without damage becomes a full width span",
60 frame: testFrame(" ", "edit", " "),
61 committed: NewFrame(4, 3),
62 want: DeltaPlan{Spans: []Span{{Y: 1, X: 0, Width: 4}}},
63 },
64 {
65 name: "adjacent and overlapping damage is ordered and merged",
66 frame: testFrame(" ", "aaaaaaaaa ", " "),
67 damage: []Damage{
68 {Kind: DamageText, X: 4, Y: 1, Width: 5, Height: 1},
69 {Kind: DamageClear, X: 0, Y: 1, Width: 5, Height: 1},
70 {Kind: DamageText, X: 2, Y: 1, Width: 2, Height: 1},
71 },
72 committed: NewFrame(10, 3),
73 want: DeltaPlan{Spans: []Span{{Y: 1, X: 0, Width: 9}}},
74 },
75 {
76 name: "safe full width scroll retains exposed span",
77 frame: scrolled,
78 damage: []Damage{
79 {Kind: DamageScrollUp, X: 0, Y: 1, Width: 4, Height: 6, Count: 1},
80 {Kind: DamageText, X: 0, Y: 6, Width: 4, Height: 1},
81 },
82 committed: scrollCommitted,
83 want: DeltaPlan{
84 Scroll: Scroll{Y: 1, Height: 6, Count: 1},
85 Spans: []Span{{Y: 6, X: 0, Width: 4}},
86 },
87 },
88 {
89 name: "safe full width scroll with expensive delta snapshots",
90 frame: testFrame("1111", "new!"),
91 damage: []Damage{
92 {Kind: DamageScrollUp, X: 0, Y: 0, Width: 4, Height: 2, Count: 1},
93 {Kind: DamageText, X: 0, Y: 0, Width: 4, Height: 2},
94 },
95 committed: testFrame("0000", "1111"),
96 want: DeltaPlan{Snapshot: true},
97 },
98 {
99 name: "rectangular scroll snapshots",
100 frame: scrolled,
101 damage: []Damage{
102 {Kind: DamageScrollUp, X: 1, Y: 1, Width: 3, Height: 6, Count: 1},
103 },
104 committed: scrollCommitted,
105 want: DeltaPlan{Snapshot: true},
106 },
107 {
108 name: "unsafe scroll content snapshots",
109 frame: testFrame("0000", "2222", "xxxx", "4444", "5555", "6666", "new!", "7777"),
110 damage: []Damage{
111 {Kind: DamageScrollUp, X: 0, Y: 1, Width: 4, Height: 6, Count: 1},
112 {Kind: DamageText, X: 0, Y: 6, Width: 4, Height: 1},
113 },
114 committed: scrollCommitted,
115 want: DeltaPlan{Snapshot: true},
116 },
117 {
118 name: "more than 4096 canonical spans snapshots",
119 frame: largeFrame,
120 damage: []Damage{{Kind: DamageText, X: 0, Y: 0, Width: 1, Height: maxPlannedDamageSpans + 1}},
121 committed: NewFrame(1, maxPlannedDamageSpans+1),
122 want: DeltaPlan{Snapshot: true},
123 },
124 {
125 name: "delta cost equal to snapshot cost snapshots",
126 frame: testFrame("changed!"),
127 damage: []Damage{{Kind: DamageText, X: 0, Y: 0, Width: 8, Height: 1}},
128 committed: NewFrame(8, 1),
129 want: DeltaPlan{Snapshot: true, Spans: []Span{{Y: 0, X: 0, Width: 8}}},
130 },
131 {
132 name: "single broad damage retains spans for snapshot encoding",
133 frame: testFrame("aaaa", "bbbb"),
134 damage: []Damage{{Kind: DamageText, X: 0, Y: 0, Width: 4, Height: 2}},
135 committed: NewFrame(4, 2),
136 want: DeltaPlan{
137 Snapshot: true,
138 Spans: []Span{{Y: 0, X: 0, Width: 4}, {Y: 1, X: 0, Width: 4}},
139 },
140 },
141 {
142 name: "snapshot cost includes style runs",
143 frame: styledFrame,
144 damage: []Damage{
145 {Kind: DamageText, X: 0, Y: 0, Width: 1, Height: 1},
146 {Kind: DamageText, X: 9, Y: 0, Width: 1, Height: 1},
147 },
148 committed: styledCommitted,
149 want: DeltaPlan{Spans: []Span{{Y: 0, X: 0, Width: 1}, {Y: 0, X: 9, Width: 1}}},
150 },
151 }
152
153 for _, tt := range tests {
154 t.Run(tt.name, func(t *testing.T) {
155 candidate, err := PlanDelta(tt.frame, tt.damage, tt.committed, tt.reset)
156 if err != nil {
157 t.Fatalf("PlanDelta() error = %v", err)
158 }
159 if !reflect.DeepEqual(candidate.Plan, tt.want) {
160 t.Fatalf("PlanDelta() = %#v, want %#v", candidate.Plan, tt.want)
161 }
162 })
163 }
164}
165
166func TestNoOpDeltaCandidateDoesNotCloneFrame(t *testing.T) {
167 frame := testFrame("abcd", "efgh")
168 candidate, err := PlanDelta(frame, nil, frame, false)
169 if err != nil {
170 t.Fatalf("PlanDelta() error = %v", err)
171 }
172 if candidate.frame.Width != 0 || candidate.frame.Height != 0 {
173 t.Fatal("no-op candidate owns frame storage")
174 }
175
176 committed := frame.Clone()
177 before := committed.Clone()
178 candidate.Commit(&committed)
179 if !reflect.DeepEqual(committed, before) {
180 t.Fatal("no-op candidate changed committed frame")
181 }
182}
183
184func TestPlanDeltaDoesNotMutateCommitted(t *testing.T) {
185 committed := testFrame("0000", "1111", "2222", "3333", "4444")
186 committed.ScrollUp(1, 3, 1)
187 before := committed.Clone()
188 frame := testFrame("0000", "2222", "3333", "new!", "4444")
189 damage := []Damage{
190 {Kind: DamageScrollUp, X: 0, Y: 1, Width: 4, Height: 3, Count: 1},
191 {Kind: DamageText, X: 0, Y: 3, Width: 4, Height: 1},
192 }
193
194 if _, err := PlanDelta(frame, damage, committed, false); err != nil {
195 t.Fatalf("PlanDelta() error = %v", err)
196 }
197
198 for y := range committed.Height {
199 if !reflect.DeepEqual(committed.Row(y), before.Row(y)) {
200 t.Fatalf("PlanDelta mutated committed row %d", y)
201 }
202 }
203}
204
205func TestDeltaCandidateCommitCopiesLogicalRows(t *testing.T) {
206 frame := testFrame("aaaa", "bbbb", "cccc", "dddd")
207 frame.ScrollUp(0, 3, 1)
208 fillFrameRow(frame, 3, "new!")
209 committed := NewFrame(4, 4)
210
211 candidate, err := PlanDelta(frame, nil, committed, true)
212 if err != nil {
213 t.Fatalf("PlanDelta() error = %v", err)
214 }
215 candidate.Commit(&committed)
216
217 for y := range frame.Height {
218 if !reflect.DeepEqual(committed.Row(y), frame.Row(y)) {
219 t.Fatalf("committed row %d = %#v, want %#v", y, committed.Row(y), frame.Row(y))
220 }
221 }
222 if err := committed.CheckInvariants(); err != nil {
223 t.Fatalf("committed frame invariants: %v", err)
224 }
225}
226
227func TestPlanDeltaRejectsInvalidFrame(t *testing.T) {
228 frame := Frame{Width: 2, Height: 1}
229 if _, err := PlanDelta(frame, nil, Frame{}, false); err == nil {
230 t.Fatal("PlanDelta accepted an invalid frame")
231 }
232}
233
234func TestDeltaCandidateCommitAppliesScrollToLogicalRows(t *testing.T) {
235 committed := testFrame("0000", "1111", "2222", "3333", "4444", "5555", "6666", "7777")
236 frame := committed.Clone()
237 frame.ScrollUp(1, 6, 1)
238 fillFrameRow(frame, 6, "new!")
239 damage := []Damage{
240 {Kind: DamageScrollUp, X: 0, Y: 1, Width: 4, Height: 6, Count: 1},
241 {Kind: DamageText, X: 0, Y: 6, Width: 4, Height: 1},
242 }
243
244 candidate, err := PlanDelta(frame, damage, committed, false)
245 if err != nil {
246 t.Fatalf("PlanDelta() error = %v", err)
247 }
248 if candidate.Plan.Scroll.Height == 0 {
249 t.Fatal("PlanDelta dropped safe scroll")
250 }
251 candidate.Commit(&committed)
252
253 for y := range frame.Height {
254 if !reflect.DeepEqual(committed.Row(y), frame.Row(y)) {
255 t.Fatalf("committed row %d = %#v, want %#v", y, committed.Row(y), frame.Row(y))
256 }
257 }
258}
259
260func fillFrameRow(frame Frame, y int, row string) {
261 for x, r := range row {
262 frame.Set(x, y, Cell{Rune: r, Style: DefaultStyle()})
263 }
264}