1package core
2
3import (
4 "fmt"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/require"
9)
10
11func TestCellPayloadValidation(t *testing.T) {
12 for _, tt := range []struct {
13 name, grapheme, link string
14 valid bool
15 }{
16 {"empty", "", "", true},
17 {"grapheme and link", "e\u0301", "https://example.com", true},
18 {"invalid utf8", "\xff", "", false},
19 {"control", "a\n", "", false},
20 {"OSC injection", "", "https://x/\x1b\\", false},
21 {"C1", "", "x\u009c", false},
22 {"grapheme limit", strings.Repeat("x", MaxGraphemeBytes+1), "", false},
23 {"link limit", "", strings.Repeat("x", MaxHyperlinkBytes+1), false},
24 } {
25 t.Run(tt.name, func(t *testing.T) {
26 p, err := NewCellPayload(tt.grapheme, tt.link)
27 if !tt.valid {
28 require.ErrorIs(t, err, ErrInvalidCellPayload)
29 return
30 }
31 require.NoError(t, err)
32 require.Equal(t, tt.grapheme, p.Grapheme())
33 require.Equal(t, tt.link, p.Hyperlink())
34 })
35 }
36}
37
38func TestPayloadOwnershipAndReclamation(t *testing.T) {
39 value, err := NewCellPayload("e\u0301", "https://example.com")
40 require.NoError(t, err)
41 cell := Cell{Rune: 'e', Style: DefaultStyle(), Payload: value}
42 source := NewFrame(3, 2)
43 baseline := source.LogicalBytes()
44 source.Set(0, 0, cell)
45 source.Set(1, 0, cell)
46 require.Len(t, source.page.payloadIndex, 1)
47 require.Equal(t, baseline+value.LogicalBytes(), source.LogicalBytes())
48 clone := source.Clone()
49 dst := NewFrame(3, 2)
50 other, err := NewCellPayload("", "https://other.example")
51 require.NoError(t, err)
52 dst.Set(0, 0, Cell{Rune: 'x', Payload: other})
53 dst.Set(1, 0, source.Cell(0, 0))
54 require.Equal(t, cell, dst.Cell(1, 0))
55 source.FillRow(0, 0, 3, BlankCell())
56 require.Empty(t, source.page.payloadIndex)
57 require.Equal(t, baseline, source.LogicalBytes())
58 require.Equal(t, cell, clone.Cell(0, 0))
59 require.Equal(t, cell, dst.Cell(1, 0))
60 for _, f := range []Frame{source, clone, dst} {
61 require.NoError(t, f.CheckInvariants())
62 }
63 clone.ScrollUp(0, 1, 1)
64 require.Empty(t, clone.page.payloadIndex)
65 require.NoError(t, clone.CheckInvariants())
66}
67
68func TestPayloadChurnIsBounded(t *testing.T) {
69 f := NewFrame(1, 1)
70 for i := range 1000 {
71 p, err := NewCellPayload("", fmt.Sprintf("https://example.com/%d", i))
72 require.NoError(t, err)
73 f.Set(0, 0, Cell{Rune: 'x', Payload: p})
74 }
75 require.LessOrEqual(t, len(f.page.payloads), 2)
76 require.Len(t, f.page.payloadIndex, 1)
77 require.NoError(t, f.CheckInvariants())
78 f.page.cells[0].payloadID = 99
79 require.Error(t, f.CheckInvariants())
80}
81
82func BenchmarkPayloadOverwrite(b *testing.B) {
83 values := make([]CellPayload, 128)
84 for i := range values {
85 var err error
86 values[i], err = NewCellPayload("e\u0301", fmt.Sprintf("https://example.com/%d", i))
87 if err != nil {
88 b.Fatal(err)
89 }
90 }
91 frame := NewFrame(1, 1)
92 i := 0
93 b.ReportAllocs()
94 for b.Loop() {
95 frame.Set(0, 0, Cell{Rune: 'e', Style: DefaultStyle(), Payload: values[i%len(values)]})
96 i++
97 }
98}