1package vt
2
3import (
4 "fmt"
5 "testing"
6
7 renderer "github.com/bnema/vev-vt/core"
8 "github.com/stretchr/testify/require"
9)
10
11// Eviction advances an uncaptured head wrapper in place. A view or snapshot
12// taken between evictions must keep its rows, accounting, and IDs.
13func TestHistoryInPlaceEvictionKeepsCapturedViewsStable(t *testing.T) {
14 tests := []struct {
15 name string
16 capture func(*History) func() ([]string, int, uint64, RowID)
17 }{
18 {name: "view", capture: func(h *History) func() ([]string, int, uint64, RowID) {
19 v := h.View()
20 return func() ([]string, int, uint64, RowID) {
21 return rangeTextsNoT(v), v.Len(), v.LogicalBytes(), v.RowID(0)
22 }
23 }},
24 {name: "snapshot", capture: func(h *History) func() ([]string, int, uint64, RowID) {
25 s := h.SnapshotView()
26 return func() ([]string, int, uint64, RowID) {
27 var texts []string
28 for i := range s.ChunkCount() {
29 c := s.Chunk(i)
30 for r := range c.len() {
31 texts = append(texts, rowText(c.row(r)))
32 }
33 }
34 texts = append(texts, rangeTextsNoT(s.Tail())...)
35 var first RowID
36 if s.ChunkCount() > 0 {
37 first = s.Chunk(0).rowIDs[0]
38 }
39 return texts, s.Len(), s.LogicalBytes(), first
40 }
41 }},
42 }
43 for _, tt := range tests {
44 t.Run(tt.name, func(t *testing.T) {
45 h := NewHistory(HistoryConfig{MaxRows: 12, MaxBytes: 1 << 20, ChunkRows: 4})
46 add := func(i int) {
47 require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%3+1), LineBound{End: 4}))
48 }
49 for i := range 14 { // two evictions: the head wrapper is now private
50 add(i)
51 }
52 read := tt.capture(h)
53 texts, rows, bytes, first := read()
54 for i := 14; i < 30; i++ { // evicts through the captured head and beyond
55 add(i)
56 }
57 gotTexts, gotRows, gotBytes, gotFirst := read()
58 require.Equal(t, texts, gotTexts)
59 require.Equal(t, rows, gotRows)
60 require.Equal(t, bytes, gotBytes)
61 require.Equal(t, first, gotFirst)
62 require.Equal(t, 12, h.Len())
63 require.Equal(t, "r018", rangeTextsNoT(h.View())[0])
64 })
65 }
66}
67
68// In-place eviction keeps logical accounting identical to wrapper replacement.
69func TestHistoryInPlaceEvictionAccountingMatchesFreshBuild(t *testing.T) {
70 h := NewHistory(HistoryConfig{MaxRows: 10, MaxBytes: 1 << 20, ChunkRows: 4})
71 for i := range 37 {
72 require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%5+1), LineBound{End: 4}))
73 }
74 view := h.View()
75 for i := range view.ChunkCount() {
76 require.NoError(t, view.Chunk(i).CheckInvariants())
77 }
78 var sum uint64
79 for i := range view.ChunkCount() {
80 sum += historyChunkLogicalBytes(view.Chunk(i))
81 }
82 require.Equal(t, sum, h.LogicalBytes())
83}
84
85// An idle pass releases spare mutable-tail capacity without changing content,
86// and later appends regrow it.
87func TestHistoryCompressIdleTrimsTailCapacity(t *testing.T) {
88 for _, tailRows := range []int{0, 3} {
89 t.Run(fmt.Sprintf("tail-%d", tailRows), func(t *testing.T) {
90 h := NewHistory(HistoryConfig{MaxRows: 64, MaxBytes: 1 << 20, ChunkRows: 8})
91 for i := range 16 + tailRows {
92 require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%4+1), LineBound{End: 4}))
93 }
94 before := rangeTextsNoT(h.View())
95 bytes := h.LogicalBytes()
96 _, err := h.CompressIdle(1)
97 require.NoError(t, err)
98 require.Equal(t, len(h.tailCells), cap(h.tailCells))
99 require.Equal(t, before, rangeTextsNoT(h.View()))
100 require.Equal(t, bytes, h.LogicalBytes())
101 require.NoError(t, h.Append(styledRow("next", 1), LineBound{End: 4}))
102 got := rangeTextsNoT(h.View())
103 require.Equal(t, append(before, "next"), got)
104 })
105 }
106}
107
108// RangeText must agree with Range on every row, including wide pairs, zero
109// runes, evicted prefixes and cold pages.
110func TestHistoryRangeTextMatchesRange(t *testing.T) {
111 build := func(cold bool) HistoryView {
112 h := NewHistory(HistoryConfig{MaxRows: 20, MaxBytes: 1 << 20, ChunkRows: 4})
113 for i := range 27 {
114 row := historyRow(fmt.Sprintf("r%02d", i))
115 row = append(row,
116 renderer.Cell{Rune: '界'}, renderer.Cell{Continuation: true}, // wide pair
117 renderer.Cell{}, // zero rune reads as a space
118 renderer.Cell{Rune: 'x'})
119 require.NoError(t, h.Append(row, LineBound{End: len(row)}))
120 }
121 if cold {
122 for range 2 {
123 _, err := h.CompressIdle(100)
124 require.NoError(t, err)
125 }
126 }
127 return h.View()
128 }
129 for _, cold := range []bool{false, true} {
130 t.Run(fmt.Sprintf("cold-%v", cold), func(t *testing.T) {
131 view := build(cold)
132 type rowText struct {
133 runes []rune
134 columns []int
135 width int
136 }
137 var want []rowText
138 require.NoError(t, view.Range(func(row []renderer.Cell) bool {
139 w := rowText{width: len(row)}
140 for x, c := range row {
141 if c.Continuation {
142 continue
143 }
144 r := c.Rune
145 if r == 0 {
146 r = ' '
147 }
148 w.runes = append(w.runes, r)
149 w.columns = append(w.columns, x)
150 }
151 want = append(want, w)
152 return true
153 }))
154 var got []rowText
155 require.NoError(t, view.RangeText(func(i int, runes []rune, columns []int, width int) bool {
156 require.Equal(t, len(got), i)
157 got = append(got, rowText{append([]rune(nil), runes...), append([]int(nil), columns...), width})
158 return true
159 }))
160 require.Equal(t, want, got)
161 require.Len(t, got, 20)
162 })
163 }
164}
165
166func TestStyleSet(t *testing.T) {
167 style := func(i int) renderer.Style { return renderer.Style{Foreground: i % 256, Bold: i >= 256} }
168 tests := []struct {
169 name string
170 input []int
171 want int
172 }{
173 {name: "empty", want: 0},
174 {name: "duplicates", input: []int{1, 1, 2, 2, 1}, want: 2},
175 {name: "inline capacity", input: seqInts(styleSetInline), want: styleSetInline},
176 {name: "spills to map", input: append(seqInts(styleSetInline+5), 0, 1, 2), want: styleSetInline + 5},
177 {name: "large", input: seqInts(400), want: 400},
178 }
179 var s styleSet
180 for _, tt := range tests {
181 t.Run(tt.name, func(t *testing.T) {
182 s.reset()
183 want := map[renderer.Style]struct{}{}
184 for _, i := range tt.input {
185 s.add(style(i))
186 want[style(i)] = struct{}{}
187 }
188 require.Equal(t, tt.want, s.len())
189 got := map[renderer.Style]struct{}{}
190 for st := range s.all() {
191 _, dup := got[st]
192 require.False(t, dup, "style yielded twice")
193 got[st] = struct{}{}
194 }
195 require.Equal(t, want, got)
196 })
197 }
198}
199
200func seqInts(n int) []int {
201 out := make([]int, n)
202 for i := range out {
203 out[i] = i
204 }
205 return out
206}
207
208func rangeTextsNoT(v HistoryView) []string {
209 var out []string
210 _ = v.Range(func(row []renderer.Cell) bool {
211 out = append(out, rowText(row))
212 return true
213 })
214 return out
215}