detached ab6686c

history_codec_test.go RawHistoryFolder

1package vt
2
3import (
4	"bytes"
5	"encoding/binary"
6	"fmt"
7	"math"
8	"testing"
9
10	renderer "github.com/bnema/vev-vt/core"
11)
12
13func TestHistoryCodecCompactBytes(t *testing.T) {
14	h := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: 1 << 20, ChunkRows: 2})
15	if err := h.Append([]renderer.Cell{{Rune: 'a', Style: renderer.DefaultStyle()}}, LineBound{End: 1, Soft: true}); err != nil {
16		t.Fatalf("append: %v", err)
17	}
18	got, err := MarshalHistory(h.SealAndView())
19	if err != nil {
20		t.Fatalf("marshal: %v", err)
21	}
22
23	want := []byte{
24		'V', 'T', 'C', '1',
25		0, 0, 0, 1, // chunks
26		0, 0, 0, 0, 0, 0, 0, 2, // next row ID
27		0, 0, 0, 1, // width
28		0, 0, 0, 1, // rows
29		0, 0, 0, 1, // implicit default style only
30		0, 0, 0, 0, // no exceptional payloads
31		0, 0, 0, 0, 0, 0, 0, 1, // row ID
32		0, 0, 0, 1, 1, // bound
33		0, 0, 0, 'a', // rune
34		0, 0, 0, 0, 0, // style ID and continuation
35	}
36	if !bytes.Equal(got, want) {
37		t.Fatalf("MarshalHistory() = % x, want % x", got, want)
38	}
39}
40
41func TestHistoryCodecRoundTripsBounds(t *testing.T) {
42	h := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: 1 << 20, ChunkRows: 2})
43	bounds := []LineBound{{End: 2, Soft: true}, {End: 1}, {End: 2, Soft: true}}
44	for _, b := range bounds {
45		if err := h.Append([]renderer.Cell{{Rune: 'a'}, {Rune: 'b'}}, b); err != nil {
46			t.Fatalf("append: %v", err)
47		}
48	}
49	blob, err := MarshalHistory(h.SealAndView())
50	if err != nil {
51		t.Fatalf("marshal: %v", err)
52	}
53	view, err := UnmarshalHistory(blob)
54	if err != nil {
55		t.Fatalf("unmarshal: %v", err)
56	}
57	for i, want := range bounds {
58		if got := view.Bound(i); got != want {
59			t.Errorf("Bound(%d) = %+v, want %+v", i, got, want)
60		}
61	}
62}
63
64func TestHistoryCodecRejectsLegacyPayloads(t *testing.T) {
65	// The previous layouts are deliberately incompatible with stable IDs.
66	legacy := []byte{
67		'V', 'T', 'H', '1', 2,
68		0, 0, 0, 1, 0, 0, 0, 1,
69		0, 0, 0, 1, 0, 0, 0, 'a',
70		0, 0, 0,
71		0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
72		0, 0, 0, 0, 0, 0,
73		0,
74		0, 0, 0, 0, 0, 0, 0, 0,
75		0, 0, 0,
76		0, 0, 0, 1, 1,
77	}
78	for _, version := range []byte{1, 2, 3} {
79		data := append([]byte(nil), legacy...)
80		data[4] = version
81		if _, err := UnmarshalHistory(data); err == nil {
82			t.Fatalf("accepted legacy history version %d", version)
83		}
84	}
85}
86
87func TestChunkCodecRoundTripsLosslessCellsAndStyles(t *testing.T) {
88	indexed := renderer.Style{
89		Bold:              true,
90		Attrs:             renderer.AttrDim | renderer.AttrUnderline | renderer.AttrStrikethrough,
91		Foreground:        123,
92		Background:        45,
93		UnderlineStyle:    renderer.UnderlineDashed,
94		HasUnderlineColor: true,
95		UnderlineColor:    201,
96	}
97	rgb := renderer.Style{
98		Italic:               true,
99		Inverse:              true,
100		Attrs:                renderer.AttrBlink,
101		HasForegroundRGB:     true,
102		ForegroundRGB:        renderer.RGB{R: 1, G: 2, B: 3},
103		HasBackgroundRGB:     true,
104		BackgroundRGB:        renderer.RGB{R: 4, G: 5, B: 6},
105		UnderlineStyle:       renderer.UnderlineCurly,
106		HasUnderlineColorRGB: true,
107		UnderlineColorRGB:    renderer.RGB{R: 7, G: 8, B: 9},
108	}
109	tests := []struct {
110		name string
111		rows [][]renderer.Cell
112	}{
113		{
114			name: "indexed RGB blank and wide continuation cells are exact",
115			rows: [][]renderer.Cell{
116				{
117					{Rune: 'I', Style: indexed},
118					{Rune: 'R', Style: rgb},
119					{Rune: ' ', Style: renderer.DefaultStyle()},
120					{Rune: '界', Style: rgb},
121					{Continuation: true, Style: rgb},
122				},
123				{
124					{Rune: ' ', Style: renderer.DefaultStyle()},
125					{Rune: 'X', Style: indexed},
126				},
127			},
128		},
129	}
130
131	for _, tt := range tests {
132		t.Run(tt.name, func(t *testing.T) {
133			history := NewHistory(HistoryConfig{MaxRows: 8, ChunkRows: 2})
134			for _, row := range tt.rows {
135				requireHistoryAppend(t, history, row)
136			}
137
138			encoded, err := MarshalHistory(history.View())
139			if err != nil {
140				t.Fatalf("marshal history: %v", err)
141			}
142			decoded, err := UnmarshalHistory(encoded)
143			if err != nil {
144				t.Fatalf("unmarshal history: %v", err)
145			}
146			assertHistoryRowsEqual(t, decoded, tt.rows)
147		})
148	}
149}
150
151func TestChunkCodecRejectsTruncatedAndTrailingPayloads(t *testing.T) {
152	history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 2})
153	requireHistoryAppend(t, history, historyRow("AAAA"))
154	requireHistoryAppend(t, history, historyRow("BBBB"))
155	encoded, err := MarshalHistory(history.View())
156	if err != nil {
157		t.Fatalf("marshal history: %v", err)
158	}
159
160	for i := range len(encoded) {
161		if _, err := UnmarshalHistory(encoded[:i]); err == nil {
162			t.Fatalf("accepted truncated compact payload prefix of length %d", i)
163		}
164	}
165	if _, err := UnmarshalHistory(append(append([]byte(nil), encoded...), 0)); err == nil {
166		t.Fatal("accepted trailing garbage after compact payload")
167	}
168}
169
170func TestChunkCodecSupportsRepresentativeHistory(t *testing.T) {
171	const (
172		rows  = 10_000
173		width = 120
174	)
175
176	encoded, err := MarshalHistory(historyViewWithDimensions(rows, width))
177	if err != nil {
178		t.Fatalf("marshal representative history: %v", err)
179	}
180	decoded, err := UnmarshalHistory(encoded)
181	if err != nil {
182		t.Fatalf("unmarshal representative history: %v", err)
183	}
184	if got := decoded.Len(); got != rows {
185		t.Fatalf("decoded rows = %d, want %d", got, rows)
186	}
187	for _, rowIndex := range []int{0, rows - 1} {
188		if got := len(decoded.Row(rowIndex)); got != width {
189			t.Fatalf("decoded row %d width = %d, want %d", rowIndex, got, width)
190		}
191	}
192}
193
194func TestChunkCodecRejectsDimensionsBeyondSupportedLimits(t *testing.T) {
195	const supportedRows = 12_000
196	tests := []struct {
197		name string
198		view HistoryView
199		data []byte
200	}{
201		{
202			name: "too many rows",
203			view: historyViewWithDimensions(supportedRows+1, 0),
204			data: historyPayloadWithDimensions(supportedRows+1, 0),
205		},
206	}
207
208	for _, tt := range tests {
209		t.Run(tt.name, func(t *testing.T) {
210			if _, err := MarshalHistory(tt.view); err == nil {
211				t.Fatal("marshal accepted dimensions beyond the supported limits")
212			}
213			if _, err := UnmarshalHistory(tt.data); err == nil {
214				t.Fatal("unmarshal accepted dimensions beyond the supported limits")
215			}
216		})
217	}
218}
219
220func TestChunkCodecRejectsAggregateCellDeclarationBeforePayloadAllocation(t *testing.T) {
221	data := aggregateCellLimitDeclaration()
222	if len(data) >= 1024 {
223		t.Fatalf("aggregate declaration length = %d, want compact fixture", len(data))
224	}
225	if _, err := PreflightHistoryBlob(data); err == nil {
226		t.Fatal("preflight accepted aggregate cell declaration")
227	}
228	if _, err := UnmarshalHistory(data); err == nil {
229		t.Fatal("unmarshal accepted aggregate cell declaration")
230	}
231}
232
233func TestChunkCodecAcceptsWideRowsWithinAggregateBudget(t *testing.T) {
234	for _, width := range []int{161, 293, math.MaxUint16} {
235		t.Run(fmt.Sprintf("width_%d", width), func(t *testing.T) {
236			view := historyViewWithDimensions(1, width)
237			encoded, err := MarshalHistory(view)
238			if err != nil {
239				t.Fatalf("marshal width %d: %v", width, err)
240			}
241			decoded, err := UnmarshalHistory(encoded)
242			if err != nil {
243				t.Fatalf("unmarshal width %d: %v", width, err)
244			}
245			if got := len(decoded.Row(0)); got != width {
246				t.Fatalf("decoded width = %d, want %d", got, width)
247			}
248			if stats, err := PreflightHistoryBlob(encoded); err != nil || stats.Cells != uint64(width) {
249				t.Fatalf("preflight stats = %+v, err = %v", stats, err)
250			}
251		})
252	}
253}
254
255func TestChunkCodecRejectsHostileChunkAndRowDeclarations(t *testing.T) {
256	tests := []struct {
257		name       string
258		chunkCount int
259		rowCount   int
260	}{
261		{name: "more than twelve thousand chunks", chunkCount: 12_001, rowCount: 1},
262		{name: "more than twelve thousand rows", chunkCount: 47, rowCount: maxHistoryChunkRows},
263	}
264
265	for _, tt := range tests {
266		t.Run(tt.name, func(t *testing.T) {
267			if _, err := UnmarshalHistory(hostileHistoryDeclarations(tt.chunkCount, tt.rowCount)); err == nil {
268				t.Fatal("unmarshal accepted resource-exhausting history declarations")
269			}
270		})
271	}
272}
273
274func historyViewWithDimensions(rowCount, width int) HistoryView {
275	totalRows := rowCount
276	row := make([]renderer.Cell, width)
277	for i := range row {
278		row[i].Rune = 'x'
279	}
280	chunks := make([]*HistoryChunk, 0, (rowCount+maxHistoryChunkRows-1)/maxHistoryChunkRows)
281	rowID := RowID(1)
282	for rowCount > 0 {
283		chunkRows := min(rowCount, maxHistoryChunkRows)
284		rows := make([][]renderer.Cell, chunkRows)
285		rowIDs := make([]RowID, chunkRows)
286		for i := range rows {
287			rows[i] = row
288			rowIDs[i] = rowID
289			rowID++
290		}
291		chunks = append(chunks, testHistoryChunk(rows, nil, rowIDs))
292		rowCount -= chunkRows
293	}
294	return HistoryView{chunks: chunks, rows: totalRows, nextRowID: rowID}
295}
296
297func historyPayloadWithDimensions(rowCount, width int) []byte {
298	chunks := (rowCount + maxHistoryChunkRows - 1) / maxHistoryChunkRows
299	data := binary.BigEndian.AppendUint32([]byte(historyMagic), uint32(chunks))
300	data = binary.BigEndian.AppendUint64(data, uint64(rowCount+1))
301	id := uint64(1)
302	for rowCount > 0 {
303		rows := min(rowCount, maxHistoryChunkRows)
304		data = binary.BigEndian.AppendUint32(data, uint32(width))
305		data = binary.BigEndian.AppendUint32(data, uint32(rows))
306		data = binary.BigEndian.AppendUint32(data, 1)
307		data = binary.BigEndian.AppendUint32(data, 0)
308		for range rows {
309			data = binary.BigEndian.AppendUint64(data, id)
310			id++
311			data = appendHistoryBound(data, LineBound{End: width})
312		}
313		data = append(data, make([]byte, rows*width*historyStoredCellBytes)...)
314		rowCount -= rows
315	}
316	return data
317}
318
319// aggregateCellLimitDeclaration declares one row, which is within the row
320// budget, but its cells exceed the aggregate budget. It deliberately omits
321// cell payload bytes: the aggregate limit must reject it before allocation.
322func aggregateCellLimitDeclaration() []byte {
323	data := binary.BigEndian.AppendUint32([]byte(historyMagic), 1)
324	data = binary.BigEndian.AppendUint64(data, 2)
325	data = binary.BigEndian.AppendUint32(data, maxHistoryCells+1)
326	data = binary.BigEndian.AppendUint32(data, 1)
327	data = binary.BigEndian.AppendUint32(data, 1)
328	return binary.BigEndian.AppendUint32(data, 0)
329}
330
331func hostileHistoryDeclarations(chunkCount, rowCount int) []byte {
332	data := binary.BigEndian.AppendUint32([]byte(historyMagic), uint32(chunkCount))
333	data = binary.BigEndian.AppendUint64(data, uint64(chunkCount*rowCount+1))
334	id := uint64(1)
335	for range chunkCount {
336		data = binary.BigEndian.AppendUint32(data, 0)
337		data = binary.BigEndian.AppendUint32(data, uint32(rowCount))
338		data = binary.BigEndian.AppendUint32(data, 1)
339		data = binary.BigEndian.AppendUint32(data, 0)
340		for range rowCount {
341			data = binary.BigEndian.AppendUint64(data, id)
342			id++
343			data = appendHistoryBound(data, LineBound{})
344		}
345	}
346	return data
347}
348
349func TestHistoryPreflightMatchesUnmarshalMalformedInput(t *testing.T) {
350	valid, err := MarshalHistory(historyViewWithDimensions(1, 1))
351	if err != nil {
352		t.Fatalf("marshal valid history: %v", err)
353	}
354	invalidRune := append([]byte(nil), valid...)
355	binary.BigEndian.PutUint32(invalidRune[len(valid)-historyStoredCellBytes:], ^uint32(0))
356	invalidUnderlineStyle := append([]byte(nil), valid...)
357	invalidUnderlineStyle[historyHeaderBytes+historyChunkHeaderBytes+25] = byte(renderer.UnderlineDashed + 1)
358
359	tests := []struct {
360		name  string
361		data  []byte
362		valid bool
363	}{
364		{name: "valid", data: valid, valid: true},
365		{name: "truncated cell", data: valid[:len(valid)-1]},
366		{name: "trailing byte", data: append(append([]byte(nil), valid...), 0)},
367		{name: "invalid rune", data: invalidRune},
368		{name: "invalid underline style", data: invalidUnderlineStyle},
369		{name: "aggregate row budget", data: hostileHistoryDeclarations(47, maxHistoryChunkRows)},
370		{name: "aggregate cell budget", data: aggregateCellLimitDeclaration()},
371		{name: "zero row count", data: hostileHistoryDeclarations(1, 0)},
372	}
373	for _, tt := range tests {
374		t.Run(tt.name, func(t *testing.T) {
375			_, preflightErr := PreflightHistoryBlob(tt.data)
376			_, unmarshalErr := UnmarshalHistory(tt.data)
377			if (preflightErr == nil) != tt.valid || (unmarshalErr == nil) != tt.valid {
378				t.Fatalf("preflight error = %v, unmarshal error = %v, want valid = %t", preflightErr, unmarshalErr, tt.valid)
379			}
380		})
381	}
382}
383
384func FuzzHistoryPreflightMatchesUnmarshal(f *testing.F) {
385	valid, err := MarshalHistory(historyViewWithDimensions(1, 1))
386	if err != nil {
387		f.Fatalf("marshal valid history: %v", err)
388	}
389	invalidRune := append([]byte(nil), valid...)
390	binary.BigEndian.PutUint32(invalidRune[len(valid)-historyStoredCellBytes:], ^uint32(0))
391	f.Add(valid)
392	f.Add(invalidRune)
393	f.Add([]byte("VTH1\x01\x00\x00\x00\x00"))
394	f.Add([]byte("not a history payload"))
395
396	f.Fuzz(func(t *testing.T, data []byte) {
397		_, preflightErr := PreflightHistoryBlob(data)
398		_, unmarshalErr := UnmarshalHistory(data)
399		if (preflightErr == nil) != (unmarshalErr == nil) {
400			t.Fatalf("preflight error = %v, unmarshal error = %v", preflightErr, unmarshalErr)
401		}
402	})
403}
404
405func assertHistoryRowsEqual(t *testing.T, view HistoryView, want [][]renderer.Cell) {
406	t.Helper()
407	if got := view.Len(); got != len(want) {
408		t.Fatalf("row count = %d, want %d", got, len(want))
409	}
410	for y, wantRow := range want {
411		gotRow := view.Row(y)
412		if len(gotRow) != len(wantRow) {
413			t.Fatalf("row %d width = %d, want %d", y, len(gotRow), len(wantRow))
414		}
415		for x, wantCell := range wantRow {
416			if !gotRow[x].Equal(wantCell) {
417				t.Fatalf("cell (%d,%d) = %#v, want %#v", x, y, gotRow[x], wantCell)
418			}
419		}
420	}
421}

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