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}