1package html
2
3import (
4 "encoding/json"
5 "math/rand"
6 "reflect"
7 "testing"
8 "unicode/utf8"
9
10 "github.com/bnema/vev-vt/core"
11 "github.com/stretchr/testify/require"
12)
13
14func TestAppendJSONStringMatchesEncodingJSON(t *testing.T) {
15 cases := []string{
16 "", " ", "A", `"`, `\`, "<", ">", "&", "\x00", "\x1f", "\x7f", "\b\f\n\r\t",
17 "\u2028", "\u2029", "界", "😀", "e\u0301", "a<b>&c\"d\\e\u2028f",
18 }
19 for r := rune(0); r < 0x300; r++ {
20 cases = append(cases, string(r))
21 }
22 for _, text := range cases {
23 want, err := json.Marshal(text)
24 require.NoError(t, err)
25 require.Equal(t, string(want), string(appendJSONString(nil, text)), "%q", text)
26 }
27}
28
29// Invalid UTF-8 never reaches the encoder (cells are validated). Should it,
30// the output must stay valid JSON decoding to the same text encoding/json
31// would produce, whatever escaping style the toolchain's encoder uses.
32func TestAppendJSONStringInvalidUTF8StaysValidJSON(t *testing.T) {
33 for _, text := range []string{"\xff", "a\xc3", "\xed\xa0\x80", "\xe2\x80", "ok\xf0\x9f"} {
34 want, err := json.Marshal(text)
35 require.NoError(t, err)
36 var wantText, gotText string
37 require.NoError(t, json.Unmarshal(want, &wantText))
38 require.NoError(t, json.Unmarshal(appendJSONString(nil, text), &gotText), "%q", text)
39 require.Equal(t, wantText, gotText, "%q", text)
40 }
41}
42
43func FuzzAppendJSONString(f *testing.F) {
44 for _, seed := range []string{"", "a<b>&c", "\u2028\u2029", "界😀", "\x00\x1f\x7f"} {
45 f.Add(seed)
46 }
47 f.Fuzz(func(t *testing.T, text string) {
48 if !utf8.ValidString(text) {
49 t.Skip("cell text is always valid UTF-8")
50 }
51 want, err := json.Marshal(text)
52 require.NoError(t, err)
53 require.Equal(t, string(want), string(appendJSONString(nil, text)))
54 })
55}
56
57func randomColor(rng *rand.Rand) Color {
58 switch rng.Intn(3) {
59 case 0:
60 return Color{Kind: ColorDefault}
61 case 1:
62 return Color{Kind: ColorIndexed, Index: uint8(rng.Intn(256))}
63 }
64 return Color{Kind: ColorRGB, RGB: RGB{R: uint8(rng.Intn(256)), G: uint8(rng.Intn(256)), B: uint8(rng.Intn(256))}}
65}
66
67func TestAppendUpdateJSONMatchesEncodingJSON(t *testing.T) {
68 rng := rand.New(rand.NewSource(1))
69 texts := []string{" ", "A", "<", "&", `"`, `\`, "界", "😀", "\x01", "\u2028"}
70 for range 300 {
71 update := Update{
72 SchemaVersion: UpdateSchemaVersion,
73 Width: rng.Intn(500),
74 Height: rng.Intn(500),
75 Snapshot: rng.Intn(2) == 0,
76 Cursor: Cursor{
77 Row: rng.Intn(100), Column: rng.Intn(100), Visible: rng.Intn(2) == 0,
78 Style: CursorStyle(rng.Intn(7)), StyleSet: rng.Intn(2) == 0,
79 },
80 }
81 if rng.Intn(5) != 0 {
82 update.Rows = make([]RowUpdate, rng.Intn(4))
83 for i := range update.Rows {
84 update.Rows[i].Row = rng.Intn(1000)
85 if rng.Intn(5) != 0 {
86 update.Rows[i].Cells = make([]CellUpdate, rng.Intn(5))
87 for j := range update.Rows[i].Cells {
88 update.Rows[i].Cells[j] = CellUpdate{Column: rng.Intn(300), Width: 1 + rng.Intn(2), Text: texts[rng.Intn(len(texts))], Style: rng.Intn(10)}
89 }
90 }
91 }
92 }
93 if rng.Intn(5) != 0 {
94 update.Styles = make([]Style, rng.Intn(4))
95 for i := range update.Styles {
96 update.Styles[i] = Style{
97 Bold: rng.Intn(2) == 0, Italic: rng.Intn(2) == 0, Inverse: rng.Intn(2) == 0,
98 Dim: rng.Intn(2) == 0, Blink: rng.Intn(2) == 0, Strikethrough: rng.Intn(2) == 0,
99 Underline: rng.Intn(2) == 0, UnderlineStyle: core.UnderlineStyle(rng.Intn(6)),
100 Foreground: randomColor(rng), Background: randomColor(rng), UnderlineColor: randomColor(rng),
101 }
102 }
103 }
104 want, err := json.Marshal(update)
105 require.NoError(t, err)
106 got, err := appendUpdateJSON(nil, update)
107 require.NoError(t, err)
108 require.Equal(t, string(want), string(got))
109 }
110}
111
112func TestAppendUpdateJSONRejectsInvalidColorKind(t *testing.T) {
113 update := Update{Styles: []Style{{Foreground: Color{Kind: 9}}}}
114 _, err := appendUpdateJSON(nil, update)
115 require.ErrorContains(t, err, "invalid color kind 9")
116}
117
118func TestPreparedJSONMatchesEncodingJSONOfUpdate(t *testing.T) {
119 frame := core.NewFrame(6, 2)
120 style := core.DefaultStyle()
121 style.Bold = true
122 style.HasForegroundRGB, style.ForegroundRGB = true, core.RGB{R: 1, G: 2, B: 3}
123 style.Background = 200
124 frame.Set(0, 0, core.Cell{Rune: '<', Style: style})
125 frame.Set(1, 0, core.Cell{Rune: '界', Style: core.DefaultStyle()})
126 frame.Set(2, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()})
127 frame.Set(3, 0, core.Cell{Rune: '😀', Style: core.DefaultStyle()})
128 frame.Set(4, 0, core.Cell{Continuation: true, Style: core.DefaultStyle()})
129 frame.Set(5, 0, core.Cell{Rune: '&', Style: core.DefaultStyle()})
130 frame.Set(0, 1, core.Cell{Rune: '"', Style: style})
131
132 renderer, err := New(Options{})
133 require.NoError(t, err)
134 prepared, err := renderer.Prepare(frame, nil, true, Cursor{Column: 2, Visible: true, Style: 3, StyleSet: true})
135 require.NoError(t, err)
136 want, err := json.Marshal(prepared.tx.update)
137 require.NoError(t, err)
138 require.Equal(t, string(want), string(prepared.JSON()))
139}
140
141// fillAll sets every field reachable from v to a distinctive non-zero value
142// (negative for signed integers) so a field added to the update types without
143// a matching encoder change shows up as a JSON mismatch.
144func fillAll(t *testing.T, v reflect.Value, colorKind ColorKind, counter *int) {
145 t.Helper()
146 *counter++
147 n := *counter
148 if v.Type() == reflect.TypeOf(Color{}) {
149 v.FieldByName("Kind").SetUint(uint64(colorKind))
150 v.FieldByName("Index").SetUint(uint64(100 + n%100))
151 fillAll(t, v.FieldByName("RGB"), colorKind, counter)
152 return
153 }
154 switch v.Kind() {
155 case reflect.Bool:
156 v.SetBool(true)
157 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
158 v.SetInt(-int64(n) - 1)
159 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
160 v.SetUint(uint64(1 + n%120))
161 case reflect.String:
162 v.SetString("t<&>\"\\界\u2028" + string(rune('a'+n%26)))
163 case reflect.Slice:
164 v.Set(reflect.MakeSlice(v.Type(), 2, 2))
165 for i := range 2 {
166 fillAll(t, v.Index(i), colorKind, counter)
167 }
168 case reflect.Struct:
169 for i := range v.NumField() {
170 require.True(t, v.Field(i).CanSet(), "%s.%s must be settable", v.Type(), v.Type().Field(i).Name)
171 fillAll(t, v.Field(i), colorKind, counter)
172 }
173 default:
174 t.Fatalf("fillAll: unsupported kind %s in %s", v.Kind(), v.Type())
175 }
176}
177
178func requireNoZeroFields(t *testing.T, v reflect.Value) {
179 t.Helper()
180 switch v.Kind() {
181 case reflect.Struct:
182 for i := range v.NumField() {
183 requireNoZeroFields(t, v.Field(i))
184 }
185 case reflect.Slice:
186 require.NotZero(t, v.Len())
187 for i := range v.Len() {
188 requireNoZeroFields(t, v.Index(i))
189 }
190 default:
191 require.False(t, v.IsZero(), "zero %s left in filled value", v.Type())
192 }
193}
194
195// TestEncoderCoversEveryUpdateField fails when a field is added to (or removed
196// from) the update types without updating appendUpdateJSON and this test.
197func TestEncoderCoversEveryUpdateField(t *testing.T) {
198 for typ, fields := range map[reflect.Type]int{
199 reflect.TypeOf(Update{}): 7,
200 reflect.TypeOf(RowUpdate{}): 2,
201 reflect.TypeOf(CellUpdate{}): 4,
202 reflect.TypeOf(Style{}): 11,
203 reflect.TypeOf(Cursor{}): 5,
204 reflect.TypeOf(RGB{}): 3,
205 reflect.TypeOf(Color{}): 3,
206 } {
207 require.Equal(t, fields, typ.NumField(), "%s field count changed: update appendUpdateJSON", typ)
208 }
209
210 for _, kind := range []ColorKind{ColorDefault, ColorIndexed, ColorRGB} {
211 var update Update
212 counter := 0
213 fillAll(t, reflect.ValueOf(&update).Elem(), kind, &counter)
214 update.Cursor.Style = 6 // keep within the documented DECSCUSR range
215 if kind != ColorDefault {
216 requireNoZeroFields(t, reflect.ValueOf(update))
217 }
218 want, err := json.Marshal(update)
219 require.NoError(t, err)
220 got, err := appendUpdateJSON(nil, update)
221 require.NoError(t, err)
222 require.Equal(t, string(want), string(got), "color kind %d", kind)
223 }
224}