v0.4.0 0b9cfda

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

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