1package kittykey
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/require"
7)
8
9func TestParse(t *testing.T) {
10 tests := []struct {
11 name string
12 in string
13 ev Event
14 ok bool
15 partial bool
16 }{
17 {name: "plain", in: "\x1b[97u", ev: Event{Code: 'a'}, ok: true},
18 {name: "ctrl", in: "\x1b[49;5u", ev: Event{Code: '1', Mods: ModCtrl}, ok: true},
19 {name: "azerty ctrl 1 with base", in: "\x1b[38::49;5u", ev: Event{Code: '&', Base: '1', Mods: ModCtrl}, ok: true},
20 {name: "shifted and base", in: "\x1b[97:65:97;2u", ev: Event{Code: 'a', Shifted: 'A', Base: 'a', Mods: ModShift}, ok: true},
21 {name: "locks stripped", in: "\x1b[97;197u", ev: Event{Code: 'a', Mods: ModCtrl}, ok: true},
22 {name: "release", in: "\x1b[97;1:3u", ev: Event{Code: 'a', Release: true}, ok: true},
23 {name: "text field ignored", in: "\x1b[97;1;97u", ev: Event{Code: 'a'}, ok: true},
24 {name: "trailing garbage", in: "\x1b[97uxyz", ev: Event{Code: 'a'}, ok: true},
25 {name: "truncated", in: "\x1b[49;5", partial: true},
26 {name: "truncated csi", in: "\x1b[", partial: true},
27 {name: "truncated esc", in: "\x1b", partial: true},
28 {name: "empty params", in: "\x1b[u"},
29 {name: "zero code", in: "\x1b[0u"},
30 {name: "mods zero", in: "\x1b[97;0u"},
31 {name: "bad event", in: "\x1b[97;1:4u"},
32 {name: "too many fields", in: "\x1b[97;1;2;3u"},
33 {name: "other final", in: "\x1b[1;5A"},
34 {name: "private reply", in: "\x1b[?5u"},
35 {name: "huge number", in: "\x1b[99999999u"},
36 {name: "not csi", in: "a"},
37 }
38 for _, tt := range tests {
39 t.Run(tt.name, func(t *testing.T) {
40 ev, n, ok, partial := Parse([]byte(tt.in))
41 require.Equal(t, tt.ok, ok)
42 require.Equal(t, tt.partial, partial)
43 if tt.ok {
44 require.Equal(t, tt.ev, ev)
45 want := len(tt.in)
46 if tt.name == "trailing garbage" {
47 want -= len("xyz")
48 }
49 require.Equal(t, want, n)
50 }
51 })
52 }
53}
54
55func TestTranslateLegacy(t *testing.T) {
56 tests := []struct {
57 name string
58 in string
59 want string
60 }{
61 {name: "legacy passthrough", in: "hello\x1b[A\x1bj\r", want: "hello\x1b[A\x1bj\r"},
62 {name: "escape", in: "\x1b[27u", want: "\x1b"},
63 {name: "alt escape", in: "\x1b[27;3u", want: "\x1b\x1b"},
64 {name: "enter", in: "\x1b[13u", want: "\r"},
65 {name: "tab", in: "\x1b[9u", want: "\t"},
66 {name: "shift tab", in: "\x1b[9;2u", want: "\x1b[Z"},
67 {name: "backspace", in: "\x1b[127u", want: "\x7f"},
68 {name: "ctrl backspace", in: "\x1b[127;5u", want: "\x08"},
69 {name: "ctrl i", in: "\x1b[105;5u", want: "\t"},
70 {name: "ctrl a", in: "\x1b[97;5u", want: "\x01"},
71 {name: "ctrl alt a", in: "\x1b[97;7u", want: "\x1b\x01"},
72 {name: "ctrl shift a", in: "\x1b[97:65;6u", want: "\x01"},
73 {name: "ctrl space", in: "\x1b[32;5u", want: "\x00"},
74 {name: "ctrl 1 has no control byte", in: "\x1b[49;5u", want: "1"},
75 {name: "ctrl 3", in: "\x1b[51;5u", want: "\x1b"},
76 {name: "azerty ctrl ampersand", in: "\x1b[38::49;5u", want: "&"},
77 {name: "cyrillic ctrl uses base", in: "\x1b[1092::97;5u", want: "\x01"},
78 {name: "shift letter with shifted", in: "\x1b[97:65;2u", want: "A"},
79 {name: "shift letter without shifted", in: "\x1b[97;2u", want: "A"},
80 {name: "alt utf8", in: "\x1b[233;3u", want: "\x1bé"},
81 {name: "super dropped", in: "\x1b[97;9u", want: ""},
82 {name: "release dropped", in: "\x1b[97;1:3u", want: ""},
83 {name: "private use dropped", in: "\x1b[57441u", want: ""},
84 {name: "keypad digit", in: "\x1b[57400u", want: "1"},
85 {name: "keypad enter", in: "\x1b[57414u", want: "\r"},
86 {name: "ctrl keypad left", in: "\x1b[57417;5u", want: "\x1b[1;5D"},
87 {name: "ctrl keypad delete", in: "\x1b[57426;5u", want: "\x1b[3;5~"},
88 {name: "keypad begin", in: "\x1b[57427u", want: "\x1b[E"},
89 {name: "keypad separator", in: "\x1b[57416u", want: ","},
90 {name: "f1 unmodified", in: "\x1b[P", want: "\x1bOP"},
91 {name: "f2 modified kept", in: "\x1b[1;5Q", want: "\x1b[1;5Q"},
92 {name: "mixed", in: "a\x1b[97;5ub\x1b[27uc", want: "a\x01b\x1bc"},
93 {name: "truncated prefix untouched", in: "x\x1b[97;5", want: "x\x1b[97;5"},
94 {name: "trailing garbage", in: "\x1b[97;5u\x1b[zz", want: "\x01\x1b[zz"},
95 {name: "paste body untouched", in: "\x1b[200~\x1b[97;5u\x1b[201~\x1b[97;5u", want: "\x1b[200~\x1b[97;5u\x1b[201~\x01"},
96 {name: "unterminated paste untouched", in: "\x1b[200~\x1b[97;5u", want: "\x1b[200~\x1b[97;5u"},
97 {name: "mouse untouched", in: "\x1b[<0;1;1M", want: "\x1b[<0;1;1M"},
98 {name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"},
99 }
100 for _, tt := range tests {
101 t.Run(tt.name, func(t *testing.T) {
102 require.Equal(t, []byte(tt.want), append([]byte{}, Translate([]byte(tt.in), 0)...))
103 })
104 }
105}
106
107func TestTranslateKittyReceiver(t *testing.T) {
108 tests := []struct {
109 name string
110 in string
111 flags int
112 want string
113 }{
114 {name: "same flags identity", in: "\x1b[38::49;5u", flags: OuterFlags, want: "\x1b[38::49;5u"},
115 {name: "strip alternates", in: "\x1b[38::49;5u", flags: FlagDisambiguate, want: "\x1b[38;5u"},
116 {name: "keep shifted", in: "\x1b[97:65;2u", flags: FlagDisambiguate | FlagAlternateKeys, want: "\x1b[97:65;2u"},
117 {name: "unmodified", in: "\x1b[27u", flags: FlagDisambiguate, want: "\x1b[27u"},
118 {name: "release dropped without events", in: "\x1b[97;1:3u", flags: FlagDisambiguate, want: ""},
119 {name: "release kept with events", in: "\x1b[97;1:3u", flags: FlagDisambiguate | FlagReportEvents, want: "\x1b[97;1:3u"},
120 {name: "f1 stays csi", in: "\x1b[P", flags: FlagDisambiguate, want: "\x1b[P"},
121 }
122 for _, tt := range tests {
123 t.Run(tt.name, func(t *testing.T) {
124 require.Equal(t, []byte(tt.want), append([]byte{}, Translate([]byte(tt.in), tt.flags)...))
125 })
126 }
127}
128
129func TestTranslateReturnsInputWhenUnchanged(t *testing.T) {
130 in := []byte("plain \x1b[A text")
131 out := Translate(in, 0)
132 require.Same(t, &in[0], &out[0])
133}
134
135func TestEventDigit(t *testing.T) {
136 tests := []struct {
137 ev Event
138 want int
139 ok bool
140 }{
141 {ev: Event{Code: '1'}, want: 1, ok: true},
142 {ev: Event{Code: '&', Base: '1'}, want: 1, ok: true},
143 {ev: Event{Code: 'é', Base: '2'}, want: 2, ok: true},
144 {ev: Event{Code: '0'}},
145 {ev: Event{Code: 'a'}},
146 }
147 for _, tt := range tests {
148 got, ok := tt.ev.Digit()
149 require.Equal(t, tt.ok, ok, "%+v", tt.ev)
150 require.Equal(t, tt.want, got)
151 }
152}
153
154func FuzzTranslate(f *testing.F) {
155 for _, seed := range []string{"\x1b[97;5u", "\x1b[38::49;5u", "\x1b[200~x\x1b[201~", "\x1b[1;5P", "a\x1b["} {
156 f.Add([]byte(seed), 0)
157 f.Add([]byte(seed), OuterFlags)
158 }
159 f.Fuzz(func(t *testing.T, data []byte, flags int) {
160 out := Translate(data, flags&0x1f)
161 // Translation never grows input by more than a bounded factor.
162 require.LessOrEqual(t, len(out), 2*len(data)+16)
163 })
164}
165
166func BenchmarkTranslateLegacyText(b *testing.B) {
167 data := []byte("the quick brown fox jumps over the lazy dog\r")
168 b.ReportAllocs()
169 for b.Loop() {
170 _ = Translate(data, 0)
171 }
172}