1package vt
2
3import (
4 "bytes"
5 "encoding/binary"
6 "testing"
7
8 renderer "github.com/bnema/vev-vt/core"
9 "github.com/stretchr/testify/require"
10)
11
12func TestCompactHistoryDictionaryAndAccounting(t *testing.T) {
13 h := NewHistory(HistoryConfig{MaxRows: 4, MaxBytes: 1 << 20, ChunkRows: 2})
14 style := renderer.DefaultStyle()
15 style.Bold = true
16 style.HasForegroundRGB = true
17 style.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3}
18 row := []renderer.Cell{{Rune: 'a', Style: style}, {Rune: 'b', Style: style}}
19 for range 2 {
20 require.NoError(t, h.Append(row, LineBound{End: 2}))
21 }
22 view := h.SealAndView()
23 blob, err := MarshalHistory(view)
24 require.NoError(t, err)
25 require.True(t, bytes.HasPrefix(blob, []byte("VTC1")))
26 require.Len(t, blob, historyHeaderBytes+historyChunkHeaderBytes+historyStyleBytes+2*historyRowBytes+4*historyStoredCellBytes)
27 stats, err := PreflightHistoryBlob(blob)
28 require.NoError(t, err)
29 require.Equal(t, uint64(2), stats.Styles)
30 require.Equal(t, view.LogicalBytes(), stats.Bytes)
31 decoded, err := UnmarshalHistory(blob)
32 require.NoError(t, err)
33 require.Equal(t, view.LogicalBytes(), decoded.LogicalBytes())
34 require.NoError(t, decoded.Chunk(0).CheckInvariants())
35 again, err := MarshalHistory(decoded)
36 require.NoError(t, err)
37 require.Equal(t, blob, again)
38}
39
40func TestCompactHistoryRejectsMalformedReferences(t *testing.T) {
41 plain := historyPayloadWithDimensions(1, 2)
42 const rowOffset = historyHeaderBytes + historyChunkHeaderBytes
43 const cellOffset = rowOffset + historyRowBytes
44 for _, tt := range []struct {
45 name string
46 mutate func([]byte)
47 }{
48 {"style reference", func(b []byte) { binary.BigEndian.PutUint32(b[cellOffset+4:], 1) }},
49 {"continuation flags", func(b []byte) { b[cellOffset+8] = 2 }},
50 {"invalid rune", func(b []byte) { binary.BigEndian.PutUint32(b[cellOffset:], 0xd800) }},
51 {"zero style count", func(b []byte) { clear(b[24:28]) }},
52 {"excessive style count", func(b []byte) { binary.BigEndian.PutUint32(b[24:], ^uint32(0)) }},
53 {"excessive width", func(b []byte) { binary.BigEndian.PutUint32(b[16:], ^uint32(0)) }},
54 {"bound extent", func(b []byte) { binary.BigEndian.PutUint32(b[rowOffset+8:], 3) }},
55 {"bound flag", func(b []byte) { b[rowOffset+12] = 2 }},
56 } {
57 t.Run(tt.name, func(t *testing.T) {
58 b := bytes.Clone(plain)
59 tt.mutate(b)
60 _, err := PreflightHistoryBlob(b)
61 require.Error(t, err)
62 _, err = UnmarshalHistory(b)
63 require.Error(t, err)
64 })
65 }
66}
67
68func TestCompactHistoryRejectsMalformedDictionaries(t *testing.T) {
69 style := renderer.DefaultStyle()
70 style.Bold = true
71 other := style
72 other.Italic = true
73 makeBlob := func(styles []renderer.Style, refs ...uint32) []byte {
74 b := binary.BigEndian.AppendUint32([]byte(historyMagic), 1)
75 b = binary.BigEndian.AppendUint64(b, 2)
76 b = binary.BigEndian.AppendUint32(b, uint32(len(refs)))
77 b = binary.BigEndian.AppendUint32(b, 1)
78 b = binary.BigEndian.AppendUint32(b, uint32(len(styles)+1))
79 b = binary.BigEndian.AppendUint32(b, 0)
80 for _, s := range styles {
81 b = appendHistoryStyle(b, s)
82 }
83 b = binary.BigEndian.AppendUint64(b, 1)
84 b = appendHistoryBound(b, LineBound{End: len(refs)})
85 for _, ref := range refs {
86 b = binary.BigEndian.AppendUint32(b, 'x')
87 b = binary.BigEndian.AppendUint32(b, ref)
88 b = append(b, 0)
89 }
90 return b
91 }
92 valid := makeBlob([]renderer.Style{style, other}, 1, 2)
93 _, err := UnmarshalHistory(valid)
94 require.NoError(t, err)
95 noncanonical := bytes.Clone(valid)
96 // Unused underline color is required to be canonical zero.
97 noncanonical[historyHeaderBytes+historyChunkHeaderBytes+33] = 7
98 badFlags := bytes.Clone(valid)
99 badFlags[historyHeaderBytes+historyChunkHeaderBytes] |= 0x80
100 for _, tt := range []struct {
101 name string
102 data []byte
103 }{
104 {"duplicate default", makeBlob([]renderer.Style{renderer.DefaultStyle()}, 1)},
105 {"duplicate style", makeBlob([]renderer.Style{style, style}, 1, 2)},
106 {"unused style", makeBlob([]renderer.Style{style, other}, 1, 1)},
107 {"inactive fields", noncanonical},
108 {"reserved flags", badFlags},
109 } {
110 t.Run(tt.name, func(t *testing.T) {
111 _, err := PreflightHistoryBlob(tt.data)
112 require.Error(t, err)
113 _, err = UnmarshalHistory(tt.data)
114 require.Error(t, err)
115 })
116 }
117}
118
119func BenchmarkCompactHistoryCodec(b *testing.B) {
120 view := historyViewWithDimensions(10_000, 120)
121 blob, err := MarshalHistory(view)
122 if err != nil {
123 b.Fatal(err)
124 }
125 b.Run("encode", func(b *testing.B) {
126 b.ReportAllocs()
127 b.ReportMetric(float64(len(blob)), "encoded-B")
128 for b.Loop() {
129 if _, err := MarshalHistory(view); err != nil {
130 b.Fatal(err)
131 }
132 }
133 })
134 b.Run("decode", func(b *testing.B) {
135 b.ReportAllocs()
136 b.SetBytes(int64(len(blob)))
137 for b.Loop() {
138 if _, err := UnmarshalHistory(blob); err != nil {
139 b.Fatal(err)
140 }
141 }
142 })
143}