1package renderer
2
3import "testing"
4
5// rowRunes renders a logical row to a string for readable assertions.
6func rowRunes(f Frame, y int) string {
7 out := make([]rune, f.Width)
8 for x := range f.Width {
9 out[x] = f.At(x, y).Rune
10 }
11 return string(out)
12}
13
14// fillFrame writes one distinct rune per logical row so rotation is observable.
15func fillFrame(f Frame, rows []string) {
16 for y, s := range rows {
17 for x, r := range s {
18 f.Set(x, y, Cell{Rune: r, Style: DefaultStyle()})
19 }
20 }
21}
22
23func TestNewFrameCanonicalOffsets(t *testing.T) {
24 f := NewFrame(4, 3)
25 for y := range f.Height {
26 if f.lineOffset[y] != y*f.Width {
27 t.Fatalf("lineOffset[%d] = %d, want %d", y, f.lineOffset[y], y*f.Width)
28 }
29 }
30 if err := f.CheckInvariants(); err != nil {
31 t.Fatalf("fresh frame invariants: %v", err)
32 }
33}
34
35// TestCheckInvariantsDetectsBrokenRotation is the RED guard: a rotation that
36// duplicates a physical offset (the classic off-by-one bug) must be rejected.
37func TestCheckInvariantsDetectsBrokenRotation(t *testing.T) {
38 f := NewFrame(4, 3)
39 // Corrupt: two logical rows map to the same physical row.
40 f.lineOffset[1] = f.lineOffset[0]
41 if err := f.CheckInvariants(); err == nil {
42 t.Fatal("expected invariant violation for duplicate physical row, got nil")
43 }
44
45 f = NewFrame(4, 3)
46 f.lineOffset[0] = 1 // not a multiple of width
47 if err := f.CheckInvariants(); err == nil {
48 t.Fatal("expected invariant violation for non-multiple offset, got nil")
49 }
50
51 f = NewFrame(4, 3)
52 f.lineOffset[2] = 99 // out of range
53 if err := f.CheckInvariants(); err == nil {
54 t.Fatal("expected invariant violation for out-of-range offset, got nil")
55 }
56}
57
58func TestScrollUpEquivalence(t *testing.T) {
59 tests := []struct {
60 name string
61 width, height int
62 rows []string
63 top, bottom int
64 n int
65 want []string
66 }{
67 {
68 name: "scroll up 1 full screen", width: 3, height: 4,
69 rows: []string{"aaa", "bbb", "ccc", "ddd"},
70 top: 0, bottom: 3, n: 1,
71 want: []string{"bbb", "ccc", "ddd", " "},
72 },
73 {
74 name: "scroll up 2 full screen", width: 3, height: 4,
75 rows: []string{"aaa", "bbb", "ccc", "ddd"},
76 top: 0, bottom: 3, n: 2,
77 want: []string{"ccc", "ddd", " ", " "},
78 },
79 {
80 name: "scroll up region subset", width: 3, height: 5,
81 rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
82 top: 1, bottom: 3, n: 1,
83 want: []string{"aaa", "ccc", "ddd", " ", "eee"},
84 },
85 }
86 for _, tt := range tests {
87 t.Run(tt.name, func(t *testing.T) {
88 f := NewFrame(tt.width, tt.height)
89 fillFrame(f, tt.rows)
90 f.ScrollUp(tt.top, tt.bottom, tt.n)
91 for y := range f.Height {
92 if got := rowRunes(f, y); got != tt.want[y] {
93 t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
94 }
95 }
96 if err := f.CheckInvariants(); err != nil {
97 t.Errorf("invariants after scroll up: %v", err)
98 }
99 })
100 }
101}
102
103func TestScrollDownEquivalence(t *testing.T) {
104 tests := []struct {
105 name string
106 width, height int
107 rows []string
108 top, bottom int
109 n int
110 want []string
111 }{
112 {
113 name: "scroll down 1 full screen", width: 3, height: 4,
114 rows: []string{"aaa", "bbb", "ccc", "ddd"},
115 top: 0, bottom: 3, n: 1,
116 want: []string{" ", "aaa", "bbb", "ccc"},
117 },
118 {
119 name: "scroll down 2 region subset", width: 3, height: 5,
120 rows: []string{"aaa", "bbb", "ccc", "ddd", "eee"},
121 top: 1, bottom: 4, n: 2,
122 want: []string{"aaa", " ", " ", "bbb", "ccc"},
123 },
124 }
125 for _, tt := range tests {
126 t.Run(tt.name, func(t *testing.T) {
127 f := NewFrame(tt.width, tt.height)
128 fillFrame(f, tt.rows)
129 f.ScrollDown(tt.top, tt.bottom, tt.n)
130 for y := range f.Height {
131 if got := rowRunes(f, y); got != tt.want[y] {
132 t.Errorf("row %d = %q, want %q", y, got, tt.want[y])
133 }
134 }
135 if err := f.CheckInvariants(); err != nil {
136 t.Errorf("invariants after scroll down: %v", err)
137 }
138 })
139 }
140}
141
142// TestRotationIsPermutationUnderRandomOps drives an arbitrary sequence of
143// scrolls and asserts lineOffset stays a valid permutation throughout.
144func TestRotationIsPermutationUnderRandomOps(t *testing.T) {
145 f := NewFrame(5, 6)
146 fillFrame(f, []string{"aaaaa", "bbbbb", "ccccc", "ddddd", "eeeee", "fffff"})
147 ops := []struct {
148 up bool
149 top, bottom int
150 n int
151 }{
152 {true, 0, 5, 1}, {false, 1, 4, 2}, {true, 2, 5, 3},
153 {false, 0, 5, 1}, {true, 0, 2, 2}, {false, 3, 5, 1},
154 }
155 for i, op := range ops {
156 if op.up {
157 f.ScrollUp(op.top, op.bottom, op.n)
158 } else {
159 f.ScrollDown(op.top, op.bottom, op.n)
160 }
161 if err := f.CheckInvariants(); err != nil {
162 t.Fatalf("op %d (%+v): invariants broken: %v", i, op, err)
163 }
164 }
165}