1package ansi
2
3import (
4 "runtime"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestPreparedSnapshotCommitTransfersOnlyPrivatelyOwnedCells(t *testing.T) {
11 r := New(Capabilities{})
12 source := NewFrame(2, 1)
13 source.Set(0, 0, Cell{Rune: 'a', Style: DefaultStyle()})
14 prepared, err := r.Prepare(source, nil, true)
15 require.NoError(t, err)
16 alias := prepared
17 encoded := append([]byte(nil), prepared.Bytes()...)
18 source.Set(0, 0, Cell{Rune: 'b', Style: DefaultStyle()})
19 prepared.Commit()
20 require.Equal(t, 'a', r.committed.Cell(0, 0).Rune)
21 _, err = r.Draw(source, []Damage{FullRedraw()})
22 require.NoError(t, err)
23 alias.Commit()
24 require.Equal(t, 'b', r.committed.Cell(0, 0).Rune, "a retained draw copy cannot recommit stale cells")
25 require.Equal(t, encoded, alias.Bytes())
26}
27
28func TestRendererNeverAliasesCallerFrame(t *testing.T) {
29 r := New(Capabilities{})
30 frame := NewFrame(6, 3)
31 markFrame(&frame)
32 _, err := r.Draw(frame, []Damage{FullRedraw()})
33 require.NoError(t, err)
34
35 // Mutate the caller's frame in place after commit without drawing, then
36 // restore it: the renderer's committed state must not have followed it.
37 original := frame.Cell(2, 1)
38 frame.Set(2, 1, Cell{Rune: 'Q', Style: Style{Bold: true, Foreground: 1, Background: -1}})
39 require.Equal(t, original.Rune, r.committed.Cell(2, 1).Rune)
40 frame.Set(2, 1, original)
41 prepared, err := r.Prepare(frame, nil, false)
42 require.NoError(t, err)
43 require.Empty(t, prepared.Bytes(), "committed state followed caller mutation")
44 prepared.Commit()
45
46 // Mutate after Prepare and before Commit: the commit keeps the prepared
47 // content, so the next draw re-emits the difference.
48 frame.Set(0, 0, Cell{Rune: 'P', Style: DefaultStyle()})
49 prepared, err = r.Prepare(frame, []Damage{{Kind: DamageText, X: 0, Y: 0, Width: 1, Height: 1}}, false)
50 require.NoError(t, err)
51 frame.Set(0, 0, Cell{Rune: 'R', Style: DefaultStyle()})
52 prepared.Commit()
53 require.Equal(t, 'P', r.committed.Cell(0, 0).Rune)
54 out, err := r.Draw(frame, nil)
55 require.NoError(t, err)
56 require.Contains(t, string(out), "R")
57 require.Equal(t, 'R', r.committed.Cell(0, 0).Rune)
58 require.NoError(t, r.committed.CheckInvariants())
59}
60
61func TestDiscardedPreparedDrawLeavesCommittedStateUnchanged(t *testing.T) {
62 r := New(Capabilities{})
63 frame := NewFrame(5, 3)
64 markFrame(&frame)
65 _, err := r.Draw(frame, []Damage{FullRedraw()})
66 require.NoError(t, err)
67 committed := r.committed.Clone()
68
69 for _, damage := range [][]Damage{
70 {FullRedraw()},
71 {{Kind: DamageText, X: 0, Y: 1, Width: 5, Height: 1}},
72 nil,
73 } {
74 frame.Set(1, 1, Cell{Rune: '#', Style: Style{Italic: true, Foreground: 2, Background: -1}})
75 discarded, err := r.Prepare(frame, damage, false)
76 require.NoError(t, err)
77 require.NotEmpty(t, discarded.Bytes())
78 for y := range committed.Height {
79 require.Equal(t, committed.Row(y), r.committed.Row(y))
80 }
81 // Preparing again after a discard produces the same bytes.
82 again, err := r.Prepare(frame, damage, false)
83 require.NoError(t, err)
84 require.Equal(t, discarded.Bytes(), again.Bytes())
85 discarded.Commit() // superseded by a later Prepare: must not apply
86 for y := range committed.Height {
87 require.Equal(t, committed.Row(y), r.committed.Row(y))
88 }
89 // The stale commit invalidated every outstanding draw, so committing
90 // the newer one cannot re-establish a shadow; the next draw is a
91 // full snapshot that leaves the shadow exact.
92 again.Commit()
93 require.False(t, r.hasCommitted)
94 snapshot, err := r.Draw(frame, nil)
95 require.NoError(t, err)
96 require.NotEmpty(t, snapshot)
97 require.Equal(t, '#', r.committed.Cell(1, 1).Rune)
98 require.NoError(t, r.committed.CheckInvariants())
99
100 // Restore for the next scenario.
101 frame.Set(1, 1, committed.Cell(1, 1))
102 _, err = r.Draw(frame, nil)
103 require.NoError(t, err)
104 require.Equal(t, committed.Cell(1, 1).Rune, r.committed.Cell(1, 1).Rune)
105 }
106}
107
108func TestPreparedBytesSurviveLaterDraws(t *testing.T) {
109 r := New(Capabilities{})
110 frame := NewFrame(8, 2)
111 markFrame(&frame)
112 first, err := r.Prepare(frame, []Damage{FullRedraw()}, false)
113 require.NoError(t, err)
114 want := append([]byte(nil), first.Bytes()...)
115 first.Commit()
116 for i := range 5 {
117 frame.Set(i, 0, Cell{Rune: rune('v' + i%3), Style: DefaultStyle()})
118 _, err = r.Draw(frame, []Damage{{Kind: DamageText, X: i, Y: 0, Width: 1, Height: 1}})
119 require.NoError(t, err)
120 }
121 require.Equal(t, want, first.Bytes())
122}
123
124func TestRendererRepeatedDrawsMatchFreshRenderer(t *testing.T) {
125 // A long mixed sequence on one renderer (buffer reuse, resizes, reset,
126 // scroll) must keep emitting exactly what a renderer with an exact
127 // reference committed frame would: full redraw after each step equals
128 // the fresh-renderer snapshot, and no-damage diffs are empty.
129 r := New(Capabilities{})
130 frame := NewFrame(7, 4)
131 markFrame(&frame)
132 steps := []func(){
133 func() { frame.Set(3, 2, Cell{Rune: 'a', Style: Style{Bold: true, Foreground: 4, Background: -1}}) },
134 func() { frame.ScrollUp(0, 3, 1); frame.FillRow(3, 0, 7, Cell{Rune: 'n', Style: DefaultStyle()}) },
135 func() { frame = NewFrame(9, 5); markFrame(&frame) },
136 func() { frame.Set(8, 4, Cell{Rune: 'z', Style: DefaultStyle()}) },
137 func() {
138 frame = NewFrame(7, 4)
139 markFrame(&frame)
140 frame.Set(0, 0, Cell{Rune: 'k', Style: DefaultStyle()})
141 },
142 }
143 for i, step := range steps {
144 step()
145 if i == 3 {
146 r.Reset()
147 }
148 _, err := r.Draw(frame, nil)
149 require.NoError(t, err)
150 for y := range frame.Height {
151 require.Equal(t, frame.Row(y), r.committed.Row(y), "step %d row %d", i, y)
152 }
153 out, err := r.Draw(frame, nil)
154 require.NoError(t, err)
155 require.Empty(t, out, "step %d", i)
156 // Reset keeps equivalence with a brand new renderer.
157 r2 := New(Capabilities{})
158 want, err := r2.Draw(frame, nil)
159 require.NoError(t, err)
160 r.Reset()
161 got, err := r.Draw(frame, nil)
162 require.NoError(t, err)
163 require.Equal(t, want, got, "step %d", i)
164 }
165}
166
167func TestPrepareCommitDoesNotAllocateFrameStorage(t *testing.T) {
168 r := New(Capabilities{})
169 frame := NewFrame(120, 40)
170 markFrame(&frame)
171 _, err := r.Draw(frame, []Damage{FullRedraw()})
172 require.NoError(t, err)
173 _, err = r.Draw(frame, []Damage{FullRedraw()})
174 require.NoError(t, err)
175
176 // A whole frame is ~76 KB; steady-state draws must stay far below it.
177 toggled := false
178 damage := []Damage{{Kind: DamageText, X: 60, Y: 20, Width: 1, Height: 1}}
179 allocs := testing.AllocsPerRun(20, func() {
180 toggled = !toggled
181 ch := 'X'
182 if toggled {
183 ch = 'Y'
184 }
185 frame.Set(60, 20, Cell{Rune: ch, Style: DefaultStyle()})
186 prepared, err := r.Prepare(frame, damage, false)
187 if err != nil {
188 t.Fatal(err)
189 }
190 prepared.Commit()
191 })
192 require.LessOrEqual(t, allocs, 8.0)
193 var m1, m2 runtime.MemStats
194 runtime.ReadMemStats(&m1)
195 for range 50 {
196 frame.Set(60, 20, Cell{Rune: 'W', Style: DefaultStyle()})
197 prepared, err := r.Prepare(frame, damage, false)
198 require.NoError(t, err)
199 prepared.Commit()
200 frame.Set(60, 20, Cell{Rune: 'V', Style: DefaultStyle()})
201 prepared, err = r.Prepare(frame, damage, false)
202 require.NoError(t, err)
203 prepared.Commit()
204 }
205 runtime.ReadMemStats(&m2)
206 require.Less(t, (m2.TotalAlloc-m1.TotalAlloc)/100, uint64(8*1024), "per-draw bytes allocated")
207}