perf/html-runs 1e6ecdb

recovery_transcript_test.go RawHistoryFolder

1package vt
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt/core"
7	"github.com/stretchr/testify/require"
8)
9
10var (
11	recoveryTranscriptSnapshotSink RecoveryTranscriptSnapshot
12	recoveryTranscriptScreenSink   *Screen
13)
14
15func TestRecoveryTranscriptSnapshotOwnsPrimaryCellsAndBounds(t *testing.T) {
16	s := NewScreen(4, 2)
17	s.Write([]byte("keep"))
18
19	snapshot := s.RecoveryTranscriptSnapshot()
20	s.frame.Set(0, 0, renderer.BlankCell())
21	s.buffer.boundaries[0] = LineBound{}
22
23	view := decodeRecoveryTranscript(t, snapshot)
24	require.Equal(t, []string{"keep"}, historyViewTexts(view))
25	require.Equal(t, LineBound{End: 4}, view.Bound(0))
26}
27
28func TestRecoveryTranscriptSnapshotRetainsReflowBounds(t *testing.T) {
29	s := NewScreen(5, 3)
30	s.Write([]byte("abcdefgh"))
31
32	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
33
34	require.Equal(t, []string{"abcde", "fgh  "}, historyViewTexts(view))
35	require.Equal(t, LineBound{End: 5, Soft: true}, view.Bound(0))
36	require.Equal(t, LineBound{End: 3}, view.Bound(1))
37}
38
39func TestRecoveryTranscriptSnapshotAllocationsDoNotScaleWithRetainedRows(t *testing.T) {
40	const (
41		shortHeight = 8
42		tallHeight  = 512
43	)
44	newRetainedScreen := func(height int) *Screen {
45		screen := NewScreen(4, height)
46		for y := range height {
47			screen.frame.Set(0, y, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
48			screen.buffer.boundaries[y] = LineBound{End: 1}
49		}
50		return screen
51	}
52	short, tall := newRetainedScreen(shortHeight), newRetainedScreen(tallHeight)
53
54	shortAllocs := testing.AllocsPerRun(20, func() {
55		recoveryTranscriptSnapshotSink = short.RecoveryTranscriptSnapshot()
56	})
57	tallAllocs := testing.AllocsPerRun(20, func() {
58		recoveryTranscriptSnapshotSink = tall.RecoveryTranscriptSnapshot()
59	})
60
61	// Allow a small fixed variance while rejecting the former allocation per row.
62	if tallAllocs > shortAllocs+4 {
63		t.Fatalf("snapshot allocations scale with retained rows: height %d = %.0f, height %d = %.0f", shortHeight, shortAllocs, tallHeight, tallAllocs)
64	}
65}
66
67func TestRecoveryTranscriptSnapshotOrdersPrimaryThenActiveAlternateAndHardensSeams(t *testing.T) {
68	s := NewScreen(4, 3)
69	s.Write([]byte("prim"))
70	s.buffer.boundaries[0].Soft = true
71	s.Write([]byte("\x1b[?1049h"))
72	s.Write([]byte("alt"))
73	s.buffer.boundaries[0].Soft = true
74
75	snapshot := s.RecoveryTranscriptSnapshot()
76	s.frame.Set(0, 0, renderer.BlankCell())
77	s.buffer.boundaries[0] = LineBound{}
78	s.alternate.buffer.frame.Set(0, 0, renderer.BlankCell())
79	s.alternate.buffer.boundaries[0] = LineBound{}
80
81	view := decodeRecoveryTranscript(t, snapshot)
82
83	require.Equal(t, []string{"prim", "alt "}, historyViewTexts(view))
84	require.Equal(t, LineBound{End: 4}, view.Bound(0), "saved-primary seam must be hard")
85	require.Equal(t, LineBound{End: 3}, view.Bound(1), "final alternate seam must be hard")
86}
87
88func TestRecoveryTranscriptSnapshotTrimsEachViewportIndependently(t *testing.T) {
89	s := NewScreen(4, 3)
90	s.Write([]byte("main"))
91	s.Write([]byte("\x1b[?1049h"))
92	s.Write([]byte("alt"))
93
94	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
95
96	require.Equal(t, []string{"main", "alt "}, historyViewTexts(view))
97}
98
99func TestRecoveryTranscriptSnapshotExcludesExistingBoundedHistory(t *testing.T) {
100	s := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 2})
101	require.NoError(t, s.History().Append(historyRow("old"), LineBound{End: 3}))
102	s.Write([]byte("live"))
103
104	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
105
106	require.Equal(t, []string{"live"}, historyViewTexts(view))
107}
108
109func TestRecoveryTranscriptSnapshotTrimsOnlyTrailingUntouchedRows(t *testing.T) {
110	styledBlank := renderer.BlankCell()
111	styledBlank.Style.Bold = true
112
113	tests := []struct {
114		name           string
115		candidateCell  renderer.Cell
116		candidateBound LineBound
117		wantRows       int
118		wantCell       renderer.Cell
119	}{
120		{
121			name:          "untouched default blank with zero hard bound",
122			candidateCell: renderer.BlankCell(),
123			wantRows:      1,
124		},
125		{
126			name:           "written default space",
127			candidateCell:  renderer.BlankCell(),
128			candidateBound: LineBound{End: 1},
129			wantRows:       2,
130			wantCell:       renderer.BlankCell(),
131		},
132		{
133			name:          "styled blank",
134			candidateCell: styledBlank,
135			wantRows:      2,
136			wantCell:      styledBlank,
137		},
138		{
139			name:           "soft blank",
140			candidateCell:  renderer.BlankCell(),
141			candidateBound: LineBound{Soft: true},
142			wantRows:       2,
143			wantCell:       renderer.BlankCell(),
144		},
145	}
146
147	for _, tt := range tests {
148		t.Run(tt.name, func(t *testing.T) {
149			s := NewScreen(3, 3)
150			s.frame.Set(0, 0, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
151			s.buffer.boundaries[0] = LineBound{End: 1}
152			s.frame.Set(0, 1, tt.candidateCell)
153			s.buffer.boundaries[1] = tt.candidateBound
154
155			view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
156
157			require.Equal(t, tt.wantRows, view.Len())
158			if tt.wantRows == 2 {
159				require.True(t, view.Row(1)[0].Equal(tt.wantCell))
160				require.False(t, view.Bound(1).Soft, "the retained segment's final row must be hardened")
161			}
162		})
163	}
164}
165
166func TestRecoveryTranscriptSnapshotPreservesLeadingAndInternalBlankRows(t *testing.T) {
167	s := NewScreen(3, 4)
168	s.frame.Set(0, 2, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
169	s.buffer.boundaries[2] = LineBound{End: 1}
170
171	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
172
173	require.Equal(t, []string{"   ", "   ", "x  "}, historyViewTexts(view))
174	require.Equal(t, []LineBound{{}, {}, {End: 1}}, []LineBound{view.Bound(0), view.Bound(1), view.Bound(2)})
175}
176
177func TestRecoveryTranscriptSnapshotPreservesBoundsAndHardensOnlyTheFinalRow(t *testing.T) {
178	s := NewScreen(4, 3)
179	s.buffer.boundaries = []LineBound{{End: 4, Soft: true}, {End: 2}, {End: 3, Soft: true}}
180
181	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
182
183	require.Equal(t, []LineBound{{End: 4, Soft: true}, {End: 2}, {End: 3}}, []LineBound{view.Bound(0), view.Bound(1), view.Bound(2)})
184}
185
186func TestRecoveryTranscriptSnapshotMarshalsCanonicalEmptyHistory(t *testing.T) {
187	s := NewScreen(4, 2)
188
189	got, err := s.RecoveryTranscriptSnapshot().Marshal()
190	require.NoError(t, err)
191	want, err := MarshalHistory(HistoryView{nextRowID: 3})
192	require.NoError(t, err)
193
194	require.Equal(t, want, got)
195	view, err := UnmarshalHistory(got)
196	require.NoError(t, err)
197	require.Zero(t, view.Len())
198	require.Zero(t, view.ChunkCount())
199}
200
201func TestRecoveryTranscriptSnapshotChunksMoreThan256RowsCanonically(t *testing.T) {
202	s := NewScreen(1, 257)
203	for y := range s.frame.Height {
204		s.frame.Set(0, y, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
205		s.buffer.boundaries[y] = LineBound{End: 1, Soft: true}
206	}
207
208	view := decodeRecoveryTranscript(t, s.RecoveryTranscriptSnapshot())
209
210	require.Equal(t, 257, view.Len())
211	require.Equal(t, 2, view.ChunkCount())
212	require.Equal(t, 256, view.Chunk(0).len())
213	require.Equal(t, 1, view.Chunk(1).len())
214	require.True(t, view.Bound(255).Soft, "a chunk boundary is not a transcript seam")
215	require.False(t, view.Bound(256).Soft, "the transcript's final seam must be hard")
216}
217
218func TestRecoveryTranscriptSnapshotPreservesWideCells(t *testing.T) {
219	s := NewScreen(4, 2)
220	s.Write([]byte("界"))
221	want := append([]renderer.Cell(nil), s.frame.Row(0)...)
222
223	snapshot := s.RecoveryTranscriptSnapshot()
224	for x := range s.frame.Width {
225		s.frame.Set(x, 0, renderer.BlankCell())
226	}
227
228	view := decodeRecoveryTranscript(t, snapshot)
229	require.Equal(t, want, view.Row(0))
230	require.Equal(t, LineBound{End: 2}, view.Bound(0))
231	require.True(t, view.Row(0)[1].Continuation)
232}
233
234func TestNewScreenWithRecoveryTranscriptRestoresHistoryThenTranscriptAndStartsBlank(t *testing.T) {
235	sealed, tail := recoveryHistoryBlobs(t, []string{"sealed-a", "sealed-b", "tail"})
236	transcript := recoveryTranscriptBlob(t,
237		[]string{"primary", "alternate"},
238		[]LineBound{{End: 7, Soft: true}, {End: 9, Soft: true}},
239	)
240
241	screen, err := NewScreenWithRecoveryTranscript(5, 2, HistoryConfig{MaxRows: 8, MaxBytes: 1 << 20, ChunkRows: 2}, sealed, tail, transcript)
242	require.NoError(t, err)
243
244	view := screen.History().View()
245	require.Equal(t, []string{"sealed-a", "sealed-b", "tail", "primary", "alternate"}, historyViewTexts(view))
246	require.Equal(t, LineBound{End: 7, Soft: true}, view.Bound(3))
247	require.Equal(t, LineBound{End: 9}, view.Bound(4), "the restored transcript must end at a hard seam")
248	for y := range screen.frame.Height {
249		for x := range screen.frame.Width {
250			require.True(t, screen.frame.At(x, y).Equal(renderer.BlankCell()), "cell (%d,%d) must start blank", x, y)
251		}
252	}
253	require.Equal(t, []LineBound{{}, {}}, screen.LineBounds())
254}
255
256func TestNewScreenWithRecoveryTranscriptEvictsOldestRowsWithinBounds(t *testing.T) {
257	tests := []struct {
258		name       string
259		config     HistoryConfig
260		history    []string
261		transcript []string
262		want       []string
263	}{
264		{
265			name:       "row budget",
266			config:     HistoryConfig{MaxRows: 3, MaxBytes: 1 << 20, ChunkRows: 2},
267			history:    []string{"old-1", "old-2", "old-3"},
268			transcript: []string{"new-1", "new-2"},
269			want:       []string{"old-3", "new-1", "new-2"},
270		},
271		{
272			name:       "byte budget",
273			config:     HistoryConfig{MaxRows: 10, MaxBytes: 3*renderer.StoredCellLogicalBytes + 2*renderer.RowDescriptorLogicalBytes + 2*renderer.StyleRecordLogicalBytes, ChunkRows: 4},
274			history:    []string{"aa", "b", "ccc"},
275			transcript: []string{"d", "ee"},
276			want:       []string{"d", "ee"},
277		},
278	}
279
280	for _, tt := range tests {
281		t.Run(tt.name, func(t *testing.T) {
282			sealed, tail := recoveryHistoryBlobs(t, tt.history)
283			transcript := recoveryTranscriptBlob(t, tt.transcript, nil)
284
285			screen, err := NewScreenWithRecoveryTranscript(4, 2, tt.config, sealed, tail, transcript)
286			require.NoError(t, err)
287			require.Equal(t, tt.want, historyViewTexts(screen.History().View()))
288		})
289	}
290}
291
292func TestNewScreenWithRecoveryTranscriptRejectsOversizedTranscriptRow(t *testing.T) {
293	sealed, tail := recoveryHistoryBlobs(t, []string{"ok"})
294	transcript := recoveryTranscriptBlob(t, []string{"wide"}, nil)
295
296	screen, err := NewScreenWithRecoveryTranscript(4, 2, HistoryConfig{MaxRows: 4, MaxBytes: 83}, sealed, tail, transcript)
297
298	require.ErrorIs(t, err, ErrHistoryRowTooLarge)
299	require.Nil(t, screen)
300}
301
302func BenchmarkRecoveryTranscriptSnapshot(b *testing.B) {
303	for _, tt := range []struct {
304		name      string
305		alternate bool
306		wantRows  int
307	}{
308		{name: "primary", wantRows: 40},
309		{name: "primary-and-active-alternate", alternate: true, wantRows: 80},
310	} {
311		b.Run(tt.name, func(b *testing.B) {
312			screen := NewScreen(120, 40)
313			fillRecoveryTranscriptBenchmarkFrame(screen)
314			if tt.alternate {
315				screen.Write([]byte("\x1b[?1049h"))
316				fillRecoveryTranscriptBenchmarkFrame(screen)
317			}
318
319			b.ReportAllocs()
320			b.ReportMetric(float64(tt.wantRows), "rows/snapshot")
321			b.ResetTimer()
322			for b.Loop() {
323				recoveryTranscriptSnapshotSink = screen.RecoveryTranscriptSnapshot()
324			}
325			b.StopTimer()
326			if got := decodeRecoveryTranscript(b, recoveryTranscriptSnapshotSink).Len(); got != tt.wantRows {
327				b.Fatalf("captured rows = %d, want %d", got, tt.wantRows)
328			}
329		})
330	}
331}
332
333func fillRecoveryTranscriptBenchmarkFrame(screen *Screen) {
334	for y := range screen.frame.Height {
335		for x := range screen.frame.Width {
336			screen.frame.Set(x, y, renderer.Cell{Rune: rune('a' + (x+y)%26), Style: renderer.DefaultStyle()})
337		}
338		screen.buffer.boundaries[y] = LineBound{End: screen.frame.Width}
339	}
340}
341
342func BenchmarkNewScreenWithRecoveryTranscriptManyChunks(b *testing.B) {
343	const rows = maxHistoryRows
344	source := NewScreen(1, rows)
345	for y := range rows {
346		source.frame.Set(0, y, renderer.Cell{Rune: 'x', Style: renderer.DefaultStyle()})
347		source.buffer.boundaries[y] = LineBound{End: 1}
348	}
349	transcript, err := source.RecoveryTranscriptSnapshot().Marshal()
350	require.NoError(b, err)
351	tail, err := MarshalEmptyHistoryTail()
352	require.NoError(b, err)
353	config := HistoryConfig{MaxRows: rows, MaxBytes: 1 << 30, ChunkRows: maxHistoryChunkRows}
354
355	b.ReportAllocs()
356	b.ReportMetric(float64(rows), "rows/restore")
357	b.ReportMetric(float64((rows+maxHistoryChunkRows-1)/maxHistoryChunkRows), "chunks/restore")
358	b.ResetTimer()
359	for b.Loop() {
360		recoveryTranscriptScreenSink, err = NewScreenWithRecoveryTranscript(1, 1, config, nil, tail, transcript)
361		if err != nil {
362			b.Fatal(err)
363		}
364	}
365}
366
367func recoveryHistoryBlobs(t testing.TB, texts []string) ([][]byte, []byte) {
368	t.Helper()
369	history := NewHistory(HistoryConfig{MaxRows: len(texts) + 1, MaxBytes: 1 << 20, ChunkRows: 2})
370	for _, text := range texts {
371		require.NoError(t, history.Append(historyRow(text), LineBound{End: len(text)}))
372	}
373	view := history.SnapshotView()
374	sealed := make([][]byte, view.ChunkCount())
375	for i := range sealed {
376		var err error
377		sealed[i], err = MarshalHistoryChunk(view.Chunk(i))
378		require.NoError(t, err)
379	}
380	tail, err := MarshalHistoryTail(view)
381	require.NoError(t, err)
382	return sealed, tail
383}
384
385func recoveryTranscriptBlob(t testing.TB, texts []string, bounds []LineBound) []byte {
386	t.Helper()
387	rows := make([][]renderer.Cell, len(texts))
388	cells := 0
389	for i, text := range texts {
390		rows[i] = historyRow(text)
391		cells += len(rows[i])
392	}
393	if bounds == nil {
394		bounds = make([]LineBound, len(rows))
395		for i, row := range rows {
396			bounds[i].End = len(row)
397		}
398	}
399	rowIDs := make([]RowID, len(rows))
400	for i := range rowIDs {
401		rowIDs[i] = RowID(100 + i)
402	}
403	view := HistoryView{rows: len(rows), cells: cells, nextRowID: RowID(100 + len(rows))}
404	if len(rows) > 0 {
405		view.chunks = newHistoryChunks(rows, bounds, rowIDs)
406	}
407	blob, err := MarshalHistory(view)
408	require.NoError(t, err)
409	return blob
410}
411
412func decodeRecoveryTranscript(t testing.TB, snapshot RecoveryTranscriptSnapshot) HistoryView {
413	t.Helper()
414	blob, err := snapshot.Marshal()
415	require.NoError(t, err)
416	view, err := UnmarshalHistory(blob)
417	require.NoError(t, err)
418	return view
419}

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