1package core
2
3import (
4 "reflect"
5 "testing"
6)
7
8func TestSetStyleReuse(t *testing.T) {
9 for _, rotated := range []bool{false, true} {
10 f := NewFrame(3, 2)
11 if rotated {
12 f.ScrollUp(0, 1, 1)
13 }
14 styles := []Style{DefaultStyle(), {Bold: true}, {Italic: true}, {Bold: true}, DefaultStyle()}
15 for _, style := range styles {
16 for repeat := range 3 {
17 for x := range f.Width {
18 cell := Cell{Rune: rune('a' + repeat), Style: style}
19 // Inactive RGB fields differ without changing canonical style.
20 cell.Style.ForegroundRGB = RGB{R: uint8(repeat + 1)}
21 index := f.offset(x, 0)
22 oldID := f.page.cells[index].styleID
23 sameStyle := f.page.styles[oldID].style == style.Canonical()
24 f.Set(x, 0, cell)
25 if got := f.page.cells[index].styleID; sameStyle && got != oldID {
26 t.Fatalf("equivalent style changed ID from %d to %d", oldID, got)
27 }
28 if got := f.Cell(x, 0); !got.Equal(cell) {
29 t.Fatalf("cell = %+v, want %+v", got, cell)
30 }
31 if err := f.CheckInvariants(); err != nil {
32 t.Fatal(err)
33 }
34 }
35 }
36 }
37 }
38}
39
40func BenchmarkFrameRewriteSameStyle(b *testing.B) {
41 f := NewFrame(182, 53)
42 row := make([]Cell, f.Width)
43 for x := range row {
44 row[x] = Cell{Rune: 'x', Style: Style{Bold: true}}
45 }
46 for y := range f.Height {
47 f.WriteRow(y, 0, row)
48 }
49 b.ReportAllocs()
50 for b.Loop() {
51 for y := range f.Height {
52 f.WriteRow(y, 0, row)
53 }
54 }
55}
56
57func TestStyleCacheSurvivesSlotFreeAndReuse(t *testing.T) {
58 a := Style{Foreground: 1}
59 b := Style{Foreground: 2, Bold: true}
60 f := NewFrame(4, 2)
61 check := func(step string) {
62 t.Helper()
63 if err := f.CheckInvariants(); err != nil {
64 t.Fatalf("%s: %v", step, err)
65 }
66 }
67
68 f.Set(0, 0, Cell{Rune: 'a', Style: a})
69 check("set a")
70 idA := f.page.cells[f.offset(0, 0)].styleID
71 if !f.page.styleCacheOK || f.page.styleCacheID != idA {
72 t.Fatalf("cache = (%v,%d), want live slot %d", f.page.styleCacheOK, f.page.styleCacheID, idA)
73 }
74
75 // Overwrite the only A cell so its slot is freed; the cache must not keep it.
76 f.Set(0, 0, BlankCell())
77 check("free a")
78 if f.page.styleCacheOK && !f.page.styles[f.page.styleCacheID].used {
79 t.Fatal("cache points at a freed style slot")
80 }
81
82 // Reuse the freed slot for B, then request A again: it must not alias B.
83 f.Set(1, 0, Cell{Rune: 'b', Style: b})
84 check("reuse slot for b")
85 if got := f.page.cells[f.offset(1, 0)].styleID; got != idA {
86 t.Fatalf("expected freed slot %d to be reused, got %d", idA, got)
87 }
88 f.Set(2, 0, Cell{Rune: 'a', Style: a})
89 check("set a again")
90 if got := f.Cell(2, 0).Style; got != a {
91 t.Fatalf("style after reuse = %+v, want %+v", got, a)
92 }
93 if got := f.Cell(1, 0).Style; got != b {
94 t.Fatalf("style b = %+v, want %+v", got, b)
95 }
96 if f.StyleCount() != 3 {
97 t.Fatalf("StyleCount = %d, want 3", f.StyleCount())
98 }
99
100 // Repeating the cached raw style must bump references exactly once per cell.
101 f.Set(3, 0, Cell{Rune: 'a', Style: a})
102 f.Set(0, 1, Cell{Rune: 'a', Style: a})
103 check("repeat a")
104 f.Set(2, 0, BlankCell())
105 f.Set(3, 0, BlankCell())
106 f.Set(0, 1, BlankCell())
107 check("release a")
108 if f.StyleCount() != 2 {
109 t.Fatalf("StyleCount = %d, want 2", f.StyleCount())
110 }
111}
112
113func TestStyleCacheNonCanonicalInput(t *testing.T) {
114 f := NewFrame(3, 1)
115 rgbWithStaleIndex := Style{Foreground: 7, HasForegroundRGB: true, ForegroundRGB: RGB{R: 1}, Background: -1}
116 canonical := rgbWithStaleIndex.Canonical()
117 f.Set(0, 0, Cell{Rune: 'x', Style: rgbWithStaleIndex})
118 f.Set(1, 0, Cell{Rune: 'y', Style: canonical})
119 f.Set(2, 0, Cell{Rune: 'z', Style: rgbWithStaleIndex})
120 if err := f.CheckInvariants(); err != nil {
121 t.Fatal(err)
122 }
123 if f.StyleCount() != 2 {
124 t.Fatalf("StyleCount = %d, want 2", f.StyleCount())
125 }
126 for x := range 3 {
127 if got := f.Cell(x, 0).Style; got != canonical {
128 t.Fatalf("cell %d style = %+v, want %+v", x, got, canonical)
129 }
130 }
131 // Rewriting with the non-canonical form must keep reference counts exact.
132 f.Set(0, 0, Cell{Rune: 'x', Style: rgbWithStaleIndex})
133 if err := f.CheckInvariants(); err != nil {
134 t.Fatal(err)
135 }
136}
137
138func TestStyleCacheCloneAndReplaceIndependence(t *testing.T) {
139 a := Style{Foreground: 1}
140 b := Style{Foreground: 2}
141 src := NewFrame(2, 1)
142 src.Set(0, 0, Cell{Rune: 'a', Style: a})
143 clone := src.Clone()
144 if !reflect.DeepEqual(src, clone) {
145 t.Fatal("clone is not DeepEqual to its source")
146 }
147
148 // Free A in the source and reuse its slot for B; the clone keeps A.
149 src.Set(0, 0, BlankCell())
150 src.Set(1, 0, Cell{Rune: 'b', Style: b})
151 clone.Set(1, 0, Cell{Rune: 'a', Style: a})
152 for name, f := range map[string]Frame{"src": src, "clone": clone} {
153 if err := f.CheckInvariants(); err != nil {
154 t.Fatalf("%s: %v", name, err)
155 }
156 }
157 if got := clone.Cell(0, 0).Style; got != a {
158 t.Fatalf("clone cell 0 = %+v, want %+v", got, a)
159 }
160 if got := clone.Cell(1, 0).Style; got != a {
161 t.Fatalf("clone cell 1 = %+v, want %+v", got, a)
162 }
163 if got := src.Cell(1, 0).Style; got != b {
164 t.Fatalf("src cell 1 = %+v, want %+v", got, b)
165 }
166
167 replaced := NewFrame(2, 1)
168 replaced.Set(0, 0, Cell{Rune: 'q', Style: b})
169 replaced.Replace(clone)
170 replaced.Set(0, 0, Cell{Rune: 'q', Style: b})
171 replaced.Set(1, 0, Cell{Rune: 'q', Style: b})
172 if err := replaced.CheckInvariants(); err != nil {
173 t.Fatal(err)
174 }
175 if err := clone.CheckInvariants(); err != nil || clone.Cell(0, 0).Style != a {
176 t.Fatalf("clone changed by replaced frame: %v", err)
177 }
178}
179
180func TestBlankPhysicalRowReleasesStylesAndPayloads(t *testing.T) {
181 const width = 7
182 f := NewFrame(width, 3)
183 payload, err := NewCellPayload("é", "")
184 if err != nil {
185 t.Fatal(err)
186 }
187 for x := range width {
188 f.Set(x, 0, Cell{Rune: 'x', Style: Style{Foreground: x % 3}, Payload: payload})
189 f.Set(x, 1, Cell{Rune: 'y', Style: Style{Foreground: x % 3}})
190 }
191 f.ScrollUp(0, 2, 1)
192 if err := f.CheckInvariants(); err != nil {
193 t.Fatal(err)
194 }
195 for x := range width {
196 if got := f.Cell(x, 2); got != (Cell{Rune: ' ', Style: DefaultStyle()}) {
197 t.Fatalf("blank row cell %d = %+v", x, got)
198 }
199 if got := f.Cell(x, 0); got.Rune != 'y' || !got.Payload.Empty() {
200 t.Fatalf("shifted row cell %d = %+v", x, got)
201 }
202 }
203 f.ScrollDown(0, 2, 3)
204 if err := f.CheckInvariants(); err != nil {
205 t.Fatal(err)
206 }
207 if f.StyleCount() != 1 {
208 t.Fatalf("StyleCount = %d, want 1", f.StyleCount())
209 }
210}