1package core
2
3import (
4 "math"
5 "reflect"
6 "testing"
7)
8
9func TestFrameCanonicalOffsetsAndInvariants(t *testing.T) {
10 f := NewFrame(4, 3)
11 for y := range f.Height {
12 if f.page.rows[y] != uint32(y*f.Width) {
13 t.Fatalf("row offset %d = %d, want %d", y, f.page.rows[y], y*f.Width)
14 }
15 }
16 if err := f.CheckInvariants(); err != nil {
17 t.Fatalf("fresh frame invariants: %v", err)
18 }
19}
20
21func TestFrameRowReturnsOwnedCells(t *testing.T) {
22 f := NewFrame(2, 1)
23 f.Set(0, 0, Cell{Rune: 'A'})
24
25 row := f.Row(0)
26 row[0] = BlankCell()
27
28 if got := f.Cell(0, 0).Rune; got != 'A' {
29 t.Fatalf("mutating Row result changed frame cell to %q", got)
30 }
31}
32
33func TestFrameRowMutationOperationsUseLogicalRows(t *testing.T) {
34 f := NewFrame(5, 2)
35 f.WriteRow(0, 0, []Cell{{Rune: 'A'}, {Rune: 'B'}, {Rune: 'C'}, {Rune: 'D'}, {Rune: 'E'}})
36 f.ScrollDown(0, 1, 1)
37
38 f.CopyRow(1, 2, 0, 3)
39 f.FillRow(1, 0, 2, BlankCell())
40
41 got := f.Row(1)
42 want := []rune{' ', ' ', 'A', 'B', 'C'}
43 for x, r := range want {
44 if got[x].Rune != r {
45 t.Fatalf("cell %d = %q, want %q", x, got[x].Rune, r)
46 }
47 }
48}
49
50func TestStoredCellFitsCompactTarget(t *testing.T) {
51 if got := reflect.TypeOf(storedCell{}).Size(); got < 8 || got > 16 {
52 t.Fatalf("storedCell size = %d bytes, want 8..16", got)
53 }
54}
55
56func TestFrameInternsCanonicalStylesLocally(t *testing.T) {
57 f := NewFrame(3, 1)
58 first := Style{Bold: true, Foreground: 2, ForegroundRGB: RGB{R: 9, G: 8, B: 7}}
59 second := first
60 second.ForegroundRGB = RGB{R: 1, G: 2, B: 3}
61
62 f.Set(0, 0, Cell{Rune: 'a', Style: first})
63 f.Set(1, 0, Cell{Rune: 'b', Style: second})
64 f.Set(2, 0, Cell{Rune: 'c', Style: Style{}})
65
66 if f.page.cells[0].styleID != f.page.cells[1].styleID {
67 t.Fatal("semantically equal styles received different page-local IDs")
68 }
69 if f.page.cells[2].styleID == 0 {
70 t.Fatal("zero Style collapsed into default style ID zero")
71 }
72 if got := f.StyleCount(); got != 3 {
73 t.Fatalf("StyleCount() = %d, want default plus two semantic styles", got)
74 }
75 if err := f.CheckInvariants(); err != nil {
76 t.Fatalf("frame invariants: %v", err)
77 }
78}
79
80func TestFrameStyleChurnReusesUnreferencedSlots(t *testing.T) {
81 f := NewFrame(1, 1)
82 for i := range 1000 {
83 f.Set(0, 0, Cell{Rune: 'x', Style: Style{Foreground: i}})
84 }
85 if got := f.StyleCount(); got != 2 {
86 t.Fatalf("StyleCount() after churn = %d, want default plus live style", got)
87 }
88 if got := len(f.page.styles); got > 3 {
89 t.Fatalf("style slots after churn = %d, want at most 3", got)
90 }
91 f.Set(0, 0, BlankCell())
92 if got := f.StyleCount(); got != 1 {
93 t.Fatalf("StyleCount() after clear = %d, want default only", got)
94 }
95 if err := f.CheckInvariants(); err != nil {
96 t.Fatalf("frame invariants: %v", err)
97 }
98}
99
100func TestFrameCrossPageWriteRemapsStyles(t *testing.T) {
101 src := NewFrame(2, 1)
102 src.Set(0, 0, Cell{Rune: 'a', Style: Style{Bold: true, Foreground: 4}})
103 src.Set(1, 0, Cell{Rune: 'b', Style: Style{Italic: true, Background: 7}})
104 dst := NewFrame(2, 1)
105 dst.Set(0, 0, Cell{Rune: 'x', Style: Style{Foreground: 99}})
106
107 dst.WriteRow(0, 0, src.Row(0))
108
109 for x := range 2 {
110 if !dst.Cell(x, 0).Equal(src.Cell(x, 0)) {
111 t.Fatalf("cell %d changed crossing pages: got %+v want %+v", x, dst.Cell(x, 0), src.Cell(x, 0))
112 }
113 }
114 if err := dst.CheckInvariants(); err != nil {
115 t.Fatalf("destination invariants: %v", err)
116 }
117}
118
119func TestFrameClonePreservesStructureWithoutAliasing(t *testing.T) {
120 f := NewFrame(3, 2)
121 f.ScrollUp(0, 1, 1)
122 f.Set(0, 0, Cell{Rune: 'a', Style: Style{Foreground: 1}})
123 f.Set(1, 0, Cell{Rune: 'b', Style: Style{Foreground: 2}})
124 f.Set(1, 0, BlankCell()) // leave one reusable style slot
125
126 clone := f.Clone()
127 if clone.page == f.page || &clone.page.cells[0] == &f.page.cells[0] || &clone.page.rows[0] == &f.page.rows[0] {
128 t.Fatal("Clone shares page slice storage")
129 }
130 if clone.page.rows[0] != f.page.rows[0] || clone.page.cells[clone.offset(0, 0)].styleID != f.page.cells[f.offset(0, 0)].styleID {
131 t.Fatal("Clone changed row rotation or page-local style IDs")
132 }
133 clone.Set(0, 0, Cell{Rune: 'x', Style: Style{Bold: true}})
134 if f.Cell(0, 0).Rune != 'a' {
135 t.Fatal("mutating clone changed source cells")
136 }
137 if _, ok := f.page.styleIndex[Style{Bold: true}.Canonical()]; ok {
138 t.Fatal("mutating clone changed source style map")
139 }
140 if err := clone.CheckInvariants(); err != nil {
141 t.Fatalf("clone invariants: %v", err)
142 }
143}
144
145func TestCheckInvariantsValidatesFreeStyleList(t *testing.T) {
146 newFrameWithFreeStyle := func() Frame {
147 f := NewFrame(1, 1)
148 f.Set(0, 0, Cell{Style: Style{Foreground: 1}})
149 f.Set(0, 0, BlankCell())
150 return f
151 }
152 tests := map[string]func(*Frame){
153 "duplicate": func(f *Frame) { f.page.freeStyles = append(f.page.freeStyles, f.page.freeStyles[0]) },
154 "missing": func(f *Frame) { f.page.freeStyles = nil },
155 "used": func(f *Frame) { f.page.freeStyles = append(f.page.freeStyles, 0) },
156 "out of range": func(f *Frame) {
157 f.page.freeStyles = append(f.page.freeStyles, uint32(len(f.page.styles)))
158 },
159 }
160 for name, corrupt := range tests {
161 t.Run(name, func(t *testing.T) {
162 f := newFrameWithFreeStyle()
163 corrupt(&f)
164 if err := f.CheckInvariants(); err == nil {
165 t.Fatal("CheckInvariants accepted corrupt free style list")
166 }
167 })
168 }
169}
170
171func TestFrameCellCountRejectsPortableOverflow(t *testing.T) {
172 if _, ok := frameCellCount(math.MaxInt, 2); ok {
173 t.Fatal("accepted dimensions whose product exceeds int or uint32")
174 }
175 if _, ok := frameCellCount(65_536, 65_536); ok {
176 t.Fatal("accepted dimensions whose product exceeds uint32")
177 }
178}
179
180func TestFrameLogicalBytesAreDeterministic(t *testing.T) {
181 f := NewFrame(3, 2)
182 want := uint64(6*StoredCellLogicalBytes + 2*RowDescriptorLogicalBytes + StyleRecordLogicalBytes)
183 if got := f.LogicalBytes(); got != want {
184 t.Fatalf("LogicalBytes() = %d, want %d", got, want)
185 }
186 f.Set(0, 0, Cell{Rune: 'x', Style: Style{Bold: true}})
187 if got := f.LogicalBytes(); got != want+StyleRecordLogicalBytes {
188 t.Fatalf("LogicalBytes() with one interned style = %d, want %d", got, want+StyleRecordLogicalBytes)
189 }
190}
191
192func TestCheckInvariantsDetectsBrokenRotation(t *testing.T) {
193 f := NewFrame(4, 3)
194 f.page.rows[1] = f.page.rows[0]
195 if err := f.CheckInvariants(); err == nil {
196 t.Fatal("expected duplicate physical row to be rejected")
197 }
198
199 f = NewFrame(4, 3)
200 f.page.rows[0] = 1
201 if err := f.CheckInvariants(); err == nil {
202 t.Fatal("expected non-multiple offset to be rejected")
203 }
204
205 f = NewFrame(4, 3)
206 f.page.rows[2] = 99
207 if err := f.CheckInvariants(); err == nil {
208 t.Fatal("expected out-of-range offset to be rejected")
209 }
210}
211
212func TestReplacePreservesSelfAliasedScrolledRows(t *testing.T) {
213 f := NewFrame(3, 3)
214 for y, row := range []string{"abc", "def", "ghi"} {
215 for x, r := range row {
216 f.Set(x, y, Cell{Rune: r})
217 }
218 }
219 f.ScrollUp(0, 2, 1)
220 want := [][]Cell{f.Row(0), f.Row(1), f.Row(2)}
221
222 f.Replace(f)
223
224 for y := range want {
225 got := f.Row(y)
226 for x := range want[y] {
227 if !got[x].Equal(want[y][x]) {
228 t.Fatalf("cell (%d,%d) = %+v, want %+v", x, y, got[x], want[y][x])
229 }
230 }
231 }
232 if err := f.CheckInvariants(); err != nil {
233 t.Fatalf("replaced frame invariants: %v", err)
234 }
235}
236
237func TestReplacePreservesLogicalRows(t *testing.T) {
238 dst := NewFrame(3, 3)
239 dst.Set(0, 0, Cell{Rune: 'x'})
240 dst.ScrollUp(0, 2, 1)
241
242 src := NewFrame(3, 3)
243 for y, row := range []string{"abc", "def", "ghi"} {
244 for x, r := range row {
245 src.Set(x, y, Cell{Rune: r})
246 }
247 }
248 src.ScrollUp(0, 2, 1)
249
250 dst.Replace(src)
251 for y, want := range []string{"def", "ghi", " "} {
252 for x, r := range want {
253 if got := dst.At(x, y).Rune; got != r {
254 t.Fatalf("row %d col %d = %q, want %q", y, x, got, r)
255 }
256 }
257 }
258 if err := dst.CheckInvariants(); err != nil {
259 t.Fatalf("replaced frame invariants: %v", err)
260 }
261}