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}