turbo-editors/turbo-corepublic Fork 0
v0.9.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

parser_test.go · 528 lines · 15.7 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 17h ago1package terminal
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/gdamore/tcell/v2"
8)
9
10// feed writes a byte stream to a new parser and returns the screen it drew on.
11func feed(width, height int, stream string) *Screen {
12 screen := NewScreen(width, height)
13 NewParser(screen).Write([]byte(stream)) //nolint:errcheck // Write never fails
14 return screen
15}
16
17func TestPlainTextIsWritten(t *testing.T) {
18 s := feed(20, 3, "hello")
19
20 wantLines(t, s, "hello")
21}
22
23func TestWriteAlwaysConsumesEverything(t *testing.T) {
24 // A terminal has nothing useful to do with a byte it does not understand
25 // except carry on, so Write can neither fail nor stop short.
26 p := NewParser(NewScreen(10, 2))
27 stream := []byte("a\x1b[1mb\x1b]0;title\x07c\xff")
28
29 n, err := p.Write(stream)
30
31 if err != nil {
32 t.Errorf("Write() error = %v, want nil", err)
33 }
34 if n != len(stream) {
35 t.Errorf("Write() consumed %d of %d bytes", n, len(stream))
36 }
37}
38
39func TestControlCharacters(t *testing.T) {
40 tests := []struct {
41 name string
42 stream string
43 lines []string
44 }{
45 {"carriage return", "abc\rX", []string{"Xbc"}},
46 {"line feed", "a\nb", []string{"a", " b"}},
47 {"backspace moves without erasing", "abc\bX", []string{"abX"}},
48 {"tab", "a\tb", []string{"a b"}},
49 {"vertical tab behaves as a line feed", "a\vb", []string{"a", " b"}},
50 {"form feed behaves as a line feed", "a\fb", []string{"a", " b"}},
51 {"delete prints nothing", "a\x7fb", []string{"ab"}},
52 }
53
54 for _, tc := range tests {
55 t.Run(tc.name, func(t *testing.T) {
56 wantLines(t, feed(20, 4, tc.stream), tc.lines...)
57 })
58 }
59}
60
61func TestTheBellIsReportedOnceThenForgotten(t *testing.T) {
62 p := NewParser(NewScreen(10, 2))
63
64 p.Write([]byte("a\x07b")) //nolint:errcheck
65
66 if !p.TakeBell() {
67 t.Error("TakeBell() = false after the bell rang")
68 }
69 if p.TakeBell() {
70 t.Error("TakeBell() = true a second time; it must be forgotten once taken")
71 }
72 wantLines(t, p.Screen(), "ab")
73}
74
75func TestCursorMovement(t *testing.T) {
76 tests := []struct {
77 name string
78 stream string
79 want Cursor
80 }{
81 {"CUP to a row and column", "\x1b[3;5H", Cursor{Row: 2, Col: 4}},
82 {"CUP with no parameters goes home", "\x1b[H", Cursor{}},
83 {"HVP is the same as CUP", "\x1b[2;3f", Cursor{Row: 1, Col: 2}},
84 {"CUU up", "\x1b[5;5H\x1b[2A", Cursor{Row: 2, Col: 4}},
85 {"CUD down", "\x1b[1;1H\x1b[2B", Cursor{Row: 2, Col: 0}},
86 {"CUF forward", "\x1b[1;1H\x1b[3C", Cursor{Row: 0, Col: 3}},
87 {"CUB back", "\x1b[1;6H\x1b[3D", Cursor{Row: 0, Col: 2}},
88 {"a movement with no count moves one", "\x1b[3;3H\x1b[A", Cursor{Row: 1, Col: 2}},
89 {"CHA to a column", "\x1b[3;3H\x1b[7G", Cursor{Row: 2, Col: 6}},
90 {"VPA to a row", "\x1b[3;3H\x1b[6d", Cursor{Row: 5, Col: 2}},
91 {"CNL down and home", "\x1b[1;5H\x1b[2E", Cursor{Row: 2, Col: 0}},
92 {"CPL up and home", "\x1b[5;5H\x1b[2F", Cursor{Row: 2, Col: 0}},
93 {"movement is clamped", "\x1b[99;99H", Cursor{Row: 7, Col: 19}},
94 }
95
96 for _, tc := range tests {
97 t.Run(tc.name, func(t *testing.T) {
98 if got := feed(20, 8, tc.stream).Cursor(); got != tc.want {
99 t.Errorf("Cursor() = %+v, want %+v", got, tc.want)
100 }
101 })
102 }
103}
104
105func TestEraseSequences(t *testing.T) {
106 const filled = "\x1b[1;1Haaaaa\x1b[2;1Hbbbbb\x1b[3;1Hccccc\x1b[2;3H"
107
108 tests := []struct {
109 name string
110 stream string
111 lines []string
112 }{
113 {"ED to the end", filled + "\x1b[J", []string{"aaaaa", "bb", ""}},
114 {"ED to the start", filled + "\x1b[1J", []string{"", " bb", "ccccc"}},
115 {"ED all", filled + "\x1b[2J", []string{"", "", ""}},
116 {"ED with scrollback cleared", filled + "\x1b[3J", []string{"", "", ""}},
117 {"EL to the end", filled + "\x1b[K", []string{"aaaaa", "bb", "ccccc"}},
118 {"EL to the start", filled + "\x1b[1K", []string{"aaaaa", " bb", "ccccc"}},
119 {"EL all", filled + "\x1b[2K", []string{"aaaaa", "", "ccccc"}},
120 {"ECH blanks in place", filled + "\x1b[2X", []string{"aaaaa", "bb b", "ccccc"}},
121 }
122
123 for _, tc := range tests {
124 t.Run(tc.name, func(t *testing.T) {
125 wantLines(t, feed(5, 3, tc.stream), tc.lines...)
126 })
127 }
128}
129
130func TestLineAndCharacterEditing(t *testing.T) {
131 const filled = "\x1b[1;1Hone\x1b[2;1Htwo\x1b[3;1Hsix\x1b[2;1H"
132
133 tests := []struct {
134 name string
135 stream string
136 lines []string
137 }{
138 {"IL inserts a blank line", filled + "\x1b[L", []string{"one", "", "two"}},
139 {"DL removes a line", filled + "\x1b[M", []string{"one", "six", ""}},
140 {"ICH opens cells", filled + "\x1b[2@", []string{"one", " t"}},
141 {"DCH closes cells", filled + "\x1b[1P", []string{"one", "wo"}},
142 }
143
144 for _, tc := range tests {
145 t.Run(tc.name, func(t *testing.T) {
146 wantLines(t, feed(3, 3, tc.stream), tc.lines...)
147 })
148 }
149}
150
151func TestScrollSequences(t *testing.T) {
152 const filled = "\x1b[1;1Ha\x1b[2;1Hb\x1b[3;1Hc"
153
154 wantLines(t, feed(3, 3, filled+"\x1b[S"), "b", "c", "")
155 wantLines(t, feed(3, 3, filled+"\x1b[T"), "", "a", "b")
156 wantLines(t, feed(3, 3, filled+"\x1b[2S"), "c", "", "")
157}
158
159func TestScrollRegion(t *testing.T) {
160 // Rows 2 to 3 scroll; row 1 is a status line that must stay put.
161 s := feed(6, 4, "\x1b[1;1Hhead\x1b[2;4r\x1b[4;1H\x1b[2;1Ha\x1b[3;1Hb\x1b[4;1Hc\n")
162
163 wantLines(t, s, "head", "b", "c", "")
164}
165
166func TestScrollRegionWithNoParametersOpensBackUp(t *testing.T) {
167 s := feed(6, 4, "\x1b[2;3r\x1b[r\x1b[4;1H\n")
168
169 // With the whole screen scrolling again, a line feed on the last row moves
170 // everything up rather than only rows 2 and 3.
171 if got := s.Cursor().Row; got != 3 {
172 t.Errorf("Cursor().Row = %d, want the last row", got)
173 }
174}
175
176func TestAnImpossibleScrollRegionOpensBackUp(t *testing.T) {
177 s := feed(6, 4, "\x1b[3;2r")
178
179 s.MoveTo(3, 0)
180 s.LineFeed()
181 if got := s.Cursor().Row; got != 3 {
182 t.Errorf("Cursor().Row = %d, want the region to cover the whole screen", got)
183 }
184}
185
186func TestSaveAndRestoreCursorSequences(t *testing.T) {
187 for _, stream := range []string{
188 "\x1b[3;4H\x1b7\x1b[1;1H\x1b8", // DECSC and DECRC
189 "\x1b[3;4H\x1b[s\x1b[1;1H\x1b[u",
190 "\x1b[3;4H\x1b[?1048h\x1b[1;1H\x1b[?1048l",
191 } {
192 if got := feed(10, 5, stream).Cursor(); got != (Cursor{Row: 2, Col: 3}) {
193 t.Errorf("%q left the cursor at %+v, want row 2 column 3", stream, got)
194 }
195 }
196}
197
198func TestIndexAndReverseIndex(t *testing.T) {
199 // IND and RI move by a line and keep the column; only NEL returns to the
200 // first column as well.
201 wantLines(t, feed(4, 3, "a\x1bDb"), "a", " b") // IND, down
202 wantLines(t, feed(4, 3, "\x1b[2;1Ha\x1bMb"), " b", "a") // RI, up
203 wantLines(t, feed(4, 3, "ab\x1bEc"), "ab", "c") // NEL, down and home
204}
205
206func TestResetSequence(t *testing.T) {
207 s := feed(10, 3, "text\x1b[1;31m\x1bc")
208
209 wantLines(t, s, "")
210 if got := s.Cursor(); got != (Cursor{}) {
211 t.Errorf("Cursor() = %+v, want the top left", got)
212 }
213 if _, _, attrs := s.Style().Decompose(); attrs&tcell.AttrBold != 0 {
214 t.Error("the style survived the reset")
215 }
216}
217
218func TestPrivateModes(t *testing.T) {
219 if feed(10, 3, "\x1b[?25l").CursorVisible() {
220 t.Error("the cursor is visible after being hidden")
221 }
222 if !feed(10, 3, "\x1b[?25l\x1b[?25h").CursorVisible() {
223 t.Error("the cursor stayed hidden after being shown")
224 }
225 if feed(10, 3, "\x1b[?7l").AutoWrap() {
226 t.Error("wrapping is on after being turned off")
227 }
228 if !feed(10, 3, "\x1b[?1049h").Alternate() {
229 t.Error("the alternate screen did not come up")
230 }
231 if feed(10, 3, "\x1b[?1049h\x1b[?1049l").Alternate() {
232 t.Error("the alternate screen did not go away")
233 }
234}
235
236func TestTheAlternateScreenKeepsWhatWasThere(t *testing.T) {
237 s := feed(10, 3, "before\x1b[?1049hduring\x1b[?1049l")
238
239 wantLines(t, s, "before")
240}
241
242func TestSeveralModesInOneSequence(t *testing.T) {
243 s := feed(10, 3, "\x1b[?25;7l")
244
245 if s.CursorVisible() || s.AutoWrap() {
246 t.Error("a sequence naming two modes did not turn both off")
247 }
248}
249
250func TestSGRColours(t *testing.T) {
251 tests := []struct {
252 name string
253 stream string
254 check func(*testing.T, tcell.Style)
255 }{
256 {
257 name: "a basic foreground",
258 stream: "\x1b[31m",
259 check: func(t *testing.T, style tcell.Style) {
260 if fg, _, _ := style.Decompose(); fg != tcell.ColorMaroon {
261 t.Errorf("foreground = %v, want maroon", fg)
262 }
263 },
264 },
265 {
266 name: "a basic background",
267 stream: "\x1b[44m",
268 check: func(t *testing.T, style tcell.Style) {
269 if _, bg, _ := style.Decompose(); bg != tcell.ColorNavy {
270 t.Errorf("background = %v, want navy", bg)
271 }
272 },
273 },
274 {
275 name: "a bright foreground",
276 stream: "\x1b[92m",
277 check: func(t *testing.T, style tcell.Style) {
278 if fg, _, _ := style.Decompose(); fg != tcell.ColorLime {
279 t.Errorf("foreground = %v, want lime", fg)
280 }
281 },
282 },
283 {
284 name: "a 256-colour foreground",
285 stream: "\x1b[38;5;200m",
286 check: func(t *testing.T, style tcell.Style) {
287 if fg, _, _ := style.Decompose(); fg != tcell.PaletteColor(200) {
288 t.Errorf("foreground = %v, want palette colour 200", fg)
289 }
290 },
291 },
292 {
293 name: "a 24-bit background",
294 stream: "\x1b[48;2;10;20;30m",
295 check: func(t *testing.T, style tcell.Style) {
296 if _, bg, _ := style.Decompose(); bg.Hex() != 0x0a141e {
297 t.Errorf("background = %#06x, want 0x0a141e", bg.Hex())
298 }
299 },
300 },
301 {
302 name: "a colour and an attribute together",
303 stream: "\x1b[1;31m",
304 check: func(t *testing.T, style tcell.Style) {
305 fg, _, attrs := style.Decompose()
306 if fg != tcell.ColorMaroon || attrs&tcell.AttrBold == 0 {
307 t.Errorf("style = %v with attributes %v, want bold maroon", fg, attrs)
308 }
309 },
310 },
311 {
312 name: "default colours",
313 stream: "\x1b[31;44m\x1b[39;49m",
314 check: func(t *testing.T, style tcell.Style) {
315 fg, bg, _ := style.Decompose()
316 if fg != tcell.ColorDefault || bg != tcell.ColorDefault {
317 t.Errorf("style = %v on %v, want both back to the default", fg, bg)
318 }
319 },
320 },
321 }
322
323 for _, tc := range tests {
324 t.Run(tc.name, func(t *testing.T) {
325 tc.check(t, feed(10, 3, tc.stream).Style())
326 })
327 }
328}
329
330func TestSGRAttributes(t *testing.T) {
331 tests := []struct {
332 name string
333 stream string
334 attr tcell.AttrMask
335 want bool
336 }{
337 {"bold", "\x1b[1m", tcell.AttrBold, true},
338 {"bold off", "\x1b[1m\x1b[22m", tcell.AttrBold, false},
339 {"dim", "\x1b[2m", tcell.AttrDim, true},
340 {"italic", "\x1b[3m", tcell.AttrItalic, true},
341 {"italic off", "\x1b[3m\x1b[23m", tcell.AttrItalic, false},
342 {"underline", "\x1b[4m", tcell.AttrUnderline, true},
343 {"underline off", "\x1b[4m\x1b[24m", tcell.AttrUnderline, false},
344 {"blink", "\x1b[5m", tcell.AttrBlink, true},
345 {"reverse", "\x1b[7m", tcell.AttrReverse, true},
346 {"reverse off", "\x1b[7m\x1b[27m", tcell.AttrReverse, false},
347 {"strike through", "\x1b[9m", tcell.AttrStrikeThrough, true},
348 {"reset clears everything", "\x1b[1;4;7m\x1b[0m", tcell.AttrBold, false},
349 {"a bare m resets", "\x1b[1m\x1b[m", tcell.AttrBold, false},
350 }
351
352 for _, tc := range tests {
353 t.Run(tc.name, func(t *testing.T) {
354 _, _, attrs := feed(10, 3, tc.stream).Style().Decompose()
355 if got := attrs&tc.attr != 0; got != tc.want {
356 t.Errorf("the attribute is %v, want %v", got, tc.want)
357 }
358 })
359 }
360}
361
362func TestAStyleAppliesToWhatIsWrittenAfterIt(t *testing.T) {
363 s := feed(10, 2, "a\x1b[31mb")
364
365 if fg, _, _ := s.CellAt(0, 0).Style.Decompose(); fg == tcell.ColorMaroon {
366 t.Error("the character written before the colour took it")
367 }
368 if fg, _, _ := s.CellAt(0, 1).Style.Decompose(); fg != tcell.ColorMaroon {
369 t.Error("the character written after the colour did not take it")
370 }
371}
372
373func TestAnIncompleteExtendedColourIsIgnored(t *testing.T) {
374 // Half a colour is not a colour, and must not be guessed at.
375 for _, stream := range []string{"\x1b[38m", "\x1b[38;5m", "\x1b[38;2;1;2m"} {
376 if fg, _, _ := feed(10, 2, stream).Style().Decompose(); fg != tcell.ColorDefault {
377 t.Errorf("%q set the foreground to %v, want it left alone", stream, fg)
378 }
379 }
380}
381
382func TestUTF8SplitAcrossWrites(t *testing.T) {
383 // A pseudo-terminal hands over whatever has arrived, which can be half a
384 // character.
385 screen := NewScreen(10, 2)
386 p := NewParser(screen)
387 encoded := []byte("é")
388
389 for _, b := range encoded {
390 p.Write([]byte{b}) //nolint:errcheck
391 }
392
393 wantLines(t, screen, "é")
394}
395
396func TestAnEscapeSequenceSplitAcrossWrites(t *testing.T) {
397 screen := NewScreen(10, 3)
398 p := NewParser(screen)
399
400 for _, piece := range []string{"\x1b", "[3", ";2", "H", "x"} {
401 p.Write([]byte(piece)) //nolint:errcheck
402 }
403
404 wantLines(t, screen, "", "", " x")
405}
406
407func TestInvalidBytesBecomeAReplacementCharacter(t *testing.T) {
408 s := feed(10, 2, "a\xff\xff\xff\xff\xffb")
409
410 if got := s.LineText(0); !strings.HasPrefix(got, "a") || !strings.HasSuffix(got, "b") {
411 t.Errorf("row 0 = %q, want the valid characters kept around the invalid ones", got)
412 }
413}
414
415func TestWindowTitle(t *testing.T) {
416 tests := []struct {
417 name string
418 stream string
419 want string
420 }{
421 {"set with a bell terminator", "\x1b]0;my title\x07", "my title"},
422 {"set with a string terminator", "\x1b]2;other\x1b\\", "other"},
423 {"an icon-only command is ignored", "\x1b]1;icon\x07", ""},
424 {"never set", "plain text", ""},
425 }
426
427 for _, tc := range tests {
428 t.Run(tc.name, func(t *testing.T) {
429 p := NewParser(NewScreen(20, 3))
430 p.Write([]byte(tc.stream)) //nolint:errcheck
431
432 if got := p.Title(); got != tc.want {
433 t.Errorf("Title() = %q, want %q", got, tc.want)
434 }
435 })
436 }
437}
438
439func TestAnOSCDoesNotPrintItsContents(t *testing.T) {
440 s := feed(20, 2, "a\x1b]0;never printed\x07b")
441
442 wantLines(t, s, "ab")
443}
444
445func TestAnEscapeInsideAnOSCThatIsNotATerminatorEndsIt(t *testing.T) {
446 s := feed(20, 3, "\x1b]0;half\x1b[2;1Hx")
447
448 wantLines(t, s, "", "x")
449}
450
451func TestAnUnterminatedOSCDoesNotGrowForever(t *testing.T) {
452 p := NewParser(NewScreen(20, 3))
453
454 p.Write([]byte("\x1b]0;")) //nolint:errcheck
455 p.Write([]byte(strings.Repeat("x", maxOSC+100))) //nolint:errcheck
456
457 if len(p.osc) > maxOSC {
458 t.Errorf("the buffer grew to %d bytes, want it capped at %d", len(p.osc), maxOSC)
459 }
460}
461
462func TestAFloodOfParametersDoesNotGrowForever(t *testing.T) {
463 p := NewParser(NewScreen(20, 3))
464
465 p.Write([]byte("\x1b[" + strings.Repeat("1;", maxParams+100) + "m")) //nolint:errcheck
466
467 if len(p.params) > maxParams {
468 t.Errorf("the parameters grew to %d, want them capped at %d", len(p.params), maxParams)
469 }
470}
471
472func TestUnknownSequencesAreSwallowedNotPrinted(t *testing.T) {
473 // A terminal that prints the sequences it does not implement is worse than
474 // one that drops them: the display fills with rubbish.
475 tests := []string{
476 "\x1b[6n", // a device status report, which needs a reply we do not send
477 "\x1b[>c", // secondary device attributes
478 "\x1b[?2004h", // bracketed paste
479 "\x1b[?1000h", // mouse reporting
480 "\x1b(B", // a character-set selector
481 "\x1b[1 q", // a cursor-style request, with an intermediate byte
482 "\x1bP1$r\x1b\\", // a device control string
483 }
484
485 for _, stream := range tests {
486 t.Run(stream, func(t *testing.T) {
487 s := feed(20, 3, "a"+stream+"b")
488 wantLines(t, s, "ab")
489 })
490 }
491}
492
493func TestAControlCharacterInsideASequenceIsActedOn(t *testing.T) {
494 // A real terminal acts on a carriage return wherever it appears, rather
495 // than swallowing it into the sequence being collected.
496 s := feed(10, 3, "abc\x1b[1\r2H")
497
498 if got := s.Cursor().Col; got != 0 {
499 t.Errorf("Cursor().Col = %d, want the carriage return to have been acted on", got)
500 }
501}
502
503func TestScreenReturnsTheScreenBeingDrawnOn(t *testing.T) {
504 screen := NewScreen(10, 2)
505 p := NewParser(screen)
506
507 if p.Screen() != screen {
508 t.Error("Screen() returned a different screen")
509 }
510}
511
512func TestApplicationCursorKeys(t *testing.T) {
513 // Without this, the arrow keys are encoded the wrong way inside vim and at
514 // a readline prompt, which is the most visible thing an emulator can get
515 // wrong.
516 if feed(10, 3, "").ApplicationCursor() {
517 t.Error("application cursor keys are on by default")
518 }
519 if !feed(10, 3, "\x1b[?1h").ApplicationCursor() {
520 t.Error("ESC [ ? 1 h did not turn application cursor keys on")
521 }
522 if feed(10, 3, "\x1b[?1h\x1b[?1l").ApplicationCursor() {
523 t.Error("ESC [ ? 1 l did not turn them off again")
524 }
525 if feed(10, 3, "\x1b[?1h\x1bc").ApplicationCursor() {
526 t.Error("a reset left application cursor keys on")
527 }
528}