main ab6686c

prepared_ownership_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close