v0.7.1 ff50ee4

row_id_persistence_test.go RawHistoryFolder

1package vt
2
3import (
4	"encoding/binary"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt/core"
8	"github.com/stretchr/testify/require"
9)
10
11func TestHistoryCodecRejectsMalformedRowIDsAndCounters(t *testing.T) {
12	history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 4})
13	require.NoError(t, history.Append(historyRow("a"), LineBound{End: 1}))
14	require.NoError(t, history.Append(historyRow("b"), LineBound{End: 1}))
15	encoded, err := MarshalHistory(history.SealAndView())
16	require.NoError(t, err)
17
18	styles := int(binary.BigEndian.Uint32(encoded[24:28]))
19	firstIDOffset := historyHeaderBytes + historyChunkHeaderBytes + (styles-1)*historyStyleBytes
20	secondIDOffset := firstIDOffset + historyRowBytes
21	for _, test := range []struct {
22		name   string
23		mutate func([]byte)
24	}{
25		{name: "zero row ID", mutate: func(data []byte) { binary.BigEndian.PutUint64(data[firstIDOffset:], 0) }},
26		{name: "duplicate row ID", mutate: func(data []byte) { copy(data[secondIDOffset:secondIDOffset+8], data[firstIDOffset:firstIDOffset+8]) }},
27		{name: "counter does not exceed IDs", mutate: func(data []byte) { binary.BigEndian.PutUint64(data[8:16], 2) }},
28		{name: "max counter", mutate: func(data []byte) { binary.BigEndian.PutUint64(data[8:16], ^uint64(0)) }},
29	} {
30		t.Run(test.name, func(t *testing.T) {
31			data := append([]byte(nil), encoded...)
32			test.mutate(data)
33			_, unmarshalErr := UnmarshalHistory(data)
34			require.Error(t, unmarshalErr)
35			_, preflightErr := PreflightHistoryBlob(data)
36			require.Error(t, preflightErr)
37		})
38	}
39}
40
41func TestHistoryAppendRejectsExhaustedRowIDCounter(t *testing.T) {
42	history := NewHistory(HistoryConfig{MaxRows: 1, ChunkRows: 1})
43	history.nextRowID = ^RowID(0)
44	require.Error(t, history.Append(historyRow("row"), LineBound{End: 3}))
45}
46
47func TestHistoryAppendWithIDRejectsMalformedIDs(t *testing.T) {
48	const maxPersistedRowID = ^RowID(0) - 2
49	for _, test := range []struct {
50		name     string
51		id       RowID
52		existing bool
53	}{
54		{name: "zero", id: 0},
55		{name: "duplicate", id: 7, existing: true},
56		{name: "first unpersistable", id: maxPersistedRowID + 1},
57		{name: "maximum", id: ^RowID(0)},
58	} {
59		t.Run(test.name, func(t *testing.T) {
60			history := NewHistory(HistoryConfig{MaxRows: 2, ChunkRows: 2})
61			if test.existing {
62				require.NoError(t, history.AppendWithID(historyRow("existing"), LineBound{End: 8}, test.id))
63			}
64			before := history.View()
65			require.Error(t, history.AppendWithID(historyRow("row"), LineBound{End: 3}, test.id))
66			require.Equal(t, before.Len(), history.Len())
67			require.Equal(t, before.NextRowID(), history.NextRowID())
68		})
69	}
70}
71
72func TestHistoryAppendWithIDValidatesOutOfOrderDuplicates(t *testing.T) {
73	history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 1})
74	require.NoError(t, history.AppendWithID(historyRow("high"), LineBound{End: 4}, RowID(100)))
75	require.NoError(t, history.AppendWithID(historyRow("low"), LineBound{End: 3}, RowID(7)))
76	require.Equal(t, RowID(101), history.NextRowID())
77
78	before := history.View()
79	require.Error(t, history.AppendWithID(historyRow("duplicate"), LineBound{End: 9}, RowID(7)))
80	require.Equal(t, before.Len(), history.Len())
81	require.Equal(t, before.NextRowID(), history.NextRowID())
82}
83
84func TestHistoryAutomaticAllocationStaysAboveAcceptedIDsAfterEviction(t *testing.T) {
85	history := NewHistory(HistoryConfig{MaxRows: 2, ChunkRows: 1})
86	require.NoError(t, history.AppendWithID(historyRow("high"), LineBound{End: 4}, RowID(100)))
87	require.NoError(t, history.AppendWithID(historyRow("low"), LineBound{End: 3}, RowID(7)))
88	require.NoError(t, history.AppendWithID(historyRow("next-low"), LineBound{End: 8}, RowID(8)))
89	require.NoError(t, history.AppendWithID(historyRow("evict"), LineBound{End: 5}, RowID(9)))
90	require.Equal(t, RowID(101), history.NextRowID())
91
92	require.NoError(t, history.Append(historyRow("automatic"), LineBound{End: 9}))
93	view := history.View()
94	require.Equal(t, RowID(101), view.RowID(view.Len()-1))
95	require.Equal(t, RowID(102), history.NextRowID())
96	for i := range view.Len() {
97		require.Less(t, view.RowID(i), history.NextRowID())
98	}
99}
100
101func TestHistoryAutomaticAllocationBoundary(t *testing.T) {
102	lastPersistableID := ^RowID(0) - 2
103	history := NewHistory(HistoryConfig{MaxRows: 2, ChunkRows: 1})
104	history.nextRowID = lastPersistableID
105
106	require.NoError(t, history.Append(historyRow("last"), LineBound{End: 4}))
107	require.Equal(t, lastPersistableID+1, history.NextRowID())
108	before := history.View()
109	require.Error(t, history.Append(historyRow("exhausted"), LineBound{End: 9}))
110	require.Equal(t, before.Len(), history.Len())
111	require.Equal(t, before.NextRowID(), history.NextRowID())
112}
113
114func TestScreenRowIDBoundaryMatchesHistoryPersistence(t *testing.T) {
115	const maxPersistedRowID = ^RowID(0) - 2
116	for _, test := range []struct {
117		name    string
118		current RowID
119		want    RowID
120		persist bool
121	}{
122		{name: "last persistable ID", current: maxPersistedRowID - 1, want: maxPersistedRowID, persist: true},
123		{name: "first non-persistable ID", current: maxPersistedRowID, want: maxPersistedRowID + 1},
124	} {
125		t.Run(test.name, func(t *testing.T) {
126			screen := NewScreen(1, 1)
127			screen.nextRowID = test.current
128			allocate := func() {
129				require.Equal(t, test.want, screen.nextRowIDValue())
130			}
131			if !test.persist {
132				require.Panics(t, allocate)
133				return
134			}
135
136			allocate()
137			row := historyRow("x")
138			screen.buffer.rowIDs[0] = test.want
139			screen.buffer.frame.Set(0, 0, row[0])
140			screen.buffer.boundaries[0] = LineBound{End: 1}
141			transcript, err := screen.RecoveryTranscriptSnapshot().Marshal()
142			require.NoError(t, err)
143			view, err := UnmarshalHistory(transcript)
144			require.NoError(t, err)
145			require.Equal(t, test.want, view.RowID(0))
146			require.Equal(t, test.want+1, view.NextRowID())
147
148			history := NewHistory(HistoryConfig{MaxRows: 1, ChunkRows: 1})
149			require.NoError(t, history.AppendWithID(row, LineBound{End: 1}, screen.RowID(0)))
150			_, err = MarshalHistory(history.SealAndView())
151			require.NoError(t, err)
152		})
153	}
154}
155
156func TestHistoryCodecRejectsMissingRowIDsOnMarshal(t *testing.T) {
157	_, err := MarshalHistory(HistoryView{
158		chunks: []*HistoryChunk{testHistoryChunk([][]renderer.Cell{historyRow("row")}, []LineBound{{End: 3}}, nil)},
159		rows:   1,
160	})
161	require.Error(t, err)
162}
163
164func TestRestoredScreenAllocatesAbovePersistedRowIDs(t *testing.T) {
165	history := NewHistory(HistoryConfig{MaxRows: 4, ChunkRows: 1})
166	require.NoError(t, history.AppendWithID(historyRow("old"), LineBound{End: 3}, RowID(40)))
167	sealed, tail, err := MarshalSealedHistory(history.SealAndView())
168	require.NoError(t, err)
169	transcript, err := MarshalHistory(HistoryView{
170		chunks:    []*HistoryChunk{testHistoryChunk([][]renderer.Cell{historyRow("live")}, []LineBound{{End: 4}}, []RowID{50})},
171		rows:      1,
172		nextRowID: 51,
173	})
174	require.NoError(t, err)
175
176	screen, err := NewScreenWithRecoveryTranscript(3, 1, HistoryConfig{MaxRows: 8, ChunkRows: 2}, sealed, tail, transcript)
177	require.NoError(t, err)
178	require.Equal(t, RowID(40), screen.History().View().RowID(0))
179	require.Equal(t, RowID(50), screen.History().View().RowID(1))
180	require.Equal(t, RowID(51), screen.History().NextRowID())
181	require.Greater(t, screen.RowID(0), RowID(50))
182
183	before := screen.RowID(0)
184	screen.Write([]byte("\x1b[2J"))
185	require.Greater(t, screen.RowID(0), before)
186}
187
188func TestHistoryRestoreRejectsDuplicateIDsAcrossEvictedHistoryAndTranscript(t *testing.T) {
189	sealedView := HistoryView{
190		chunks:    []*HistoryChunk{testHistoryChunk([][]renderer.Cell{historyRow("old")}, []LineBound{{End: 3}}, []RowID{7})},
191		rows:      1,
192		nextRowID: 8,
193	}
194	sealed, err := MarshalHistoryChunk(sealedView.Chunk(0))
195	require.NoError(t, err)
196	tail, err := MarshalEmptyHistoryTail()
197	require.NoError(t, err)
198	transcript, err := MarshalHistory(HistoryView{
199		chunks:    []*HistoryChunk{testHistoryChunk([][]renderer.Cell{historyRow("live")}, []LineBound{{End: 4}}, []RowID{7})},
200		rows:      1,
201		nextRowID: 8,
202	})
203	require.NoError(t, err)
204
205	_, err = NewScreenWithRecoveryTranscript(4, 1, HistoryConfig{MaxRows: 1, ChunkRows: 1}, [][]byte{sealed}, tail, transcript)
206	require.Error(t, err)
207}
208
209func TestHistoryRestoreRejectsDuplicateIDsAcrossBlobs(t *testing.T) {
210	first := NewHistory(HistoryConfig{MaxRows: 2, ChunkRows: 1})
211	second := NewHistory(HistoryConfig{MaxRows: 2, ChunkRows: 1})
212	require.NoError(t, first.AppendWithID(historyRow("one"), LineBound{End: 3}, RowID(7)))
213	require.NoError(t, second.AppendWithID(historyRow("two"), LineBound{End: 3}, RowID(7)))
214	firstBlob, err := MarshalHistoryChunk(first.SealAndView().Chunk(0))
215	require.NoError(t, err)
216	secondBlob, err := MarshalHistoryChunk(second.SealAndView().Chunk(0))
217	require.NoError(t, err)
218	tail, err := MarshalEmptyHistoryTail()
219	require.NoError(t, err)
220
221	_, err = HistoryFromBlobs(HistoryConfig{MaxRows: 4, ChunkRows: 1}, [][]byte{firstBlob, secondBlob}, tail)
222	require.Error(t, err)
223}

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