1package vt
2
3import (
4 "errors"
5 "testing"
6
7 renderer "github.com/bnema/vev-vt/core"
8)
9
10func TestHistoryBoundsRetainedLogicalBytes(t *testing.T) {
11 const budget = 2*(2*renderer.StoredCellLogicalBytes+renderer.RowDescriptorLogicalBytes) + renderer.StyleRecordLogicalBytes
12 history := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: budget, ChunkRows: 4})
13 for _, text := range []string{"aa", "bb", "cc"} {
14 requireHistoryAppend(t, history, historyRow(text))
15 }
16
17 if got, want := historyViewTexts(history.View()), []string{"bb", "cc"}; !equalStrings(got, want) {
18 t.Fatalf("retained rows = %#v, want %#v", got, want)
19 }
20 if got, want := history.LogicalBytes(), budget; got != want {
21 t.Fatalf("logical bytes = %d, want %d", got, want)
22 }
23 if got, want := history.View().LogicalBytes(), budget; got != want {
24 t.Fatalf("view logical bytes = %d, want %d", got, want)
25 }
26 if got, want := history.ByteCap(), budget; got != want {
27 t.Fatalf("byte capacity = %d, want %d", got, want)
28 }
29}
30
31func TestHistoryLogicalBytesDeduplicateCanonicalStylesPerSlab(t *testing.T) {
32 const budget = 2*(renderer.StoredCellLogicalBytes+renderer.RowDescriptorLogicalBytes) + 2*renderer.StyleRecordLogicalBytes
33 history := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: budget, ChunkRows: 2})
34 style := renderer.Style{Bold: true}
35 for _, r := range []rune{'a', 'b'} {
36 if err := history.Append([]renderer.Cell{{Rune: r, Style: style}}, LineBound{End: 1}); err != nil {
37 t.Fatalf("append %q: %v", r, err)
38 }
39 }
40
41 if got, want := history.LogicalBytes(), budget; got != want {
42 t.Fatalf("logical bytes = %d, want %d", got, want)
43 }
44 if got := history.SnapshotView().LogicalBytes(); got != history.LogicalBytes() {
45 t.Fatalf("snapshot logical bytes = %d, want %d", got, history.LogicalBytes())
46 }
47 if got := history.SealAndView().LogicalBytes(); got != history.LogicalBytes() {
48 t.Fatalf("sealed view logical bytes = %d, want %d", got, history.LogicalBytes())
49 }
50}
51
52func TestHistoryLogicalBytesRemainStableAfterStyledRowEvictionAndCodecRoundTrip(t *testing.T) {
53 history := NewHistory(HistoryConfig{MaxRows: 3, MaxBytes: 1 << 20, ChunkRows: 3})
54 styles := []renderer.Style{{Bold: true}, renderer.DefaultStyle(), renderer.DefaultStyle(), renderer.DefaultStyle()}
55 for i, style := range styles {
56 if err := history.Append([]renderer.Cell{{Rune: rune('a' + i), Style: style}}, LineBound{End: 1}); err != nil {
57 t.Fatalf("append row %d: %v", i, err)
58 }
59 }
60
61 before := history.View()
62 blob, err := MarshalHistory(before)
63 if err != nil {
64 t.Fatalf("marshal history: %v", err)
65 }
66 after, err := UnmarshalHistory(blob)
67 if err != nil {
68 t.Fatalf("unmarshal history: %v", err)
69 }
70 if got, want := after.LogicalBytes(), before.LogicalBytes(); got != want {
71 t.Fatalf("round-trip logical bytes = %d, want %d", got, want)
72 }
73}
74
75func TestHistoryRejectsSingleRowLargerThanLogicalByteBudget(t *testing.T) {
76 history := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: 79, ChunkRows: 4})
77 row := []renderer.Cell{{Rune: 'x', Style: renderer.Style{Bold: true}}}
78
79 err := history.Append(row, LineBound{End: 1})
80 if !errors.Is(err, ErrHistoryRowTooLarge) {
81 t.Fatalf("append oversized row error = %v, want ErrHistoryRowTooLarge", err)
82 }
83 if history.Len() != 0 || history.LogicalBytes() != 0 {
84 t.Fatalf("rejected append mutated history: rows=%d bytes=%d", history.Len(), history.LogicalBytes())
85 }
86}
87
88func BenchmarkHistoryByteLimitedAppend(b *testing.B) {
89 const rows, width = 256, 120
90 maxBytes := uint64(rows)*(uint64(width)*renderer.StoredCellLogicalBytes+renderer.RowDescriptorLogicalBytes) + renderer.StyleRecordLogicalBytes
91 history := NewHistory(HistoryConfig{MaxRows: rows * 2, MaxBytes: maxBytes, ChunkRows: rows})
92 row := make([]renderer.Cell, width)
93 for range rows {
94 requireHistoryAppend(b, history, row)
95 }
96
97 b.ReportAllocs()
98 b.SetBytes(int64(width) * int64(renderer.StoredCellLogicalBytes))
99 b.ResetTimer()
100 for b.Loop() {
101 requireHistoryAppend(b, history, row)
102 }
103 b.StopTimer()
104 if history.LogicalBytes() > maxBytes {
105 b.Fatalf("logical bytes = %d, exceeds capacity %d", history.LogicalBytes(), maxBytes)
106 }
107}
108
109func TestHistoryRestoreEvictsToLogicalByteBudget(t *testing.T) {
110 source := NewHistory(HistoryConfig{MaxRows: 8, MaxBytes: 1 << 20, ChunkRows: 4})
111 for _, text := range []string{"aa", "bb", "cc"} {
112 requireHistoryAppend(t, source, historyRow(text))
113 }
114 sealed, tail, err := MarshalSealedHistory(source.SealAndView())
115 if err != nil {
116 t.Fatalf("marshal history: %v", err)
117 }
118
119 const budget = 2*(2*renderer.StoredCellLogicalBytes+renderer.RowDescriptorLogicalBytes) + renderer.StyleRecordLogicalBytes
120 restored, err := HistoryFromBlobs(HistoryConfig{MaxRows: 8, MaxBytes: budget, ChunkRows: 4}, sealed, tail)
121 if err != nil {
122 t.Fatalf("restore history: %v", err)
123 }
124 if got, want := historyViewTexts(restored.View()), []string{"bb", "cc"}; !equalStrings(got, want) {
125 t.Fatalf("restored rows = %#v, want %#v", got, want)
126 }
127 if got := restored.LogicalBytes(); got > restored.ByteCap() {
128 t.Fatalf("restored logical bytes = %d, exceeds capacity %d", got, restored.ByteCap())
129 }
130}