v0.6.0 b250c5e

encode_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close