Files
fid/tty_test.go

242 lines
6.2 KiB
Go

package main
import (
"os"
"testing"
"time"
)
func withKeyReader(t *testing.T, send func(w *os.File), check func(kr *keyReader)) {
t.Helper()
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer r.Close()
kr := newKeyReader(r)
go func() {
send(w)
w.Close()
}()
check(kr)
}
func TestKeyReaderPlainRune(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.Write([]byte("a"))
}, func(kr *keyReader) {
k := kr.next()
if k.special != keyRune || k.r != 'a' {
t.Fatalf("got %+v, want rune 'a'", k)
}
})
}
func TestKeyReaderUTF8Rune(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.WriteString("ä") // 0xC3 0xA4
}, func(kr *keyReader) {
k := kr.next()
if k.special != keyRune || k.r != 'ä' {
t.Fatalf("got %+v, want rune 'ä'", k)
}
})
}
func TestKeyReaderArrows(t *testing.T) {
seqs := map[string]specialKey{
"\x1b[A": keyUp,
"\x1b[B": keyDown,
"\x1b[C": keyRight,
"\x1b[D": keyLeft,
"\x1b[H": keyHome,
"\x1b[F": keyEnd,
"\x1b[Z": keyShiftTab,
}
for seq, want := range seqs {
seq, want := seq, want
t.Run(seq, func(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.WriteString(seq)
}, func(kr *keyReader) {
k := kr.next()
if k.special != want {
t.Fatalf("seq %q: got %v, want %v", seq, k.special, want)
}
})
})
}
}
func TestKeyReaderTildeSequences(t *testing.T) {
seqs := map[string]specialKey{
"\x1b[1~": keyHome,
"\x1b[3~": keyDelete,
"\x1b[4~": keyEnd,
}
for seq, want := range seqs {
seq, want := seq, want
t.Run(seq, func(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.WriteString(seq)
}, func(kr *keyReader) {
k := kr.next()
if k.special != want {
t.Fatalf("seq %q: got %v, want %v", seq, k.special, want)
}
})
})
}
}
// Regression test for the reported bug: a modified arrow key ("ESC[1;5B",
// Ctrl+Down) is a real, standard, longer-than-plain CSI sequence. The old
// fixed-shape parser read '1' expecting a bare "ESC[1~" (Home), blindly
// discarded the next byte as "the tilde" (here it's ';', so the sequence
// wasn't actually finished), and misreported Home - leaving "5B" queued to
// leak out as two literal runes on the next reads instead of being
// recognized as part of the key sequence.
func TestKeyReaderModifiedArrow(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.WriteString("\x1b[1;5BZ") // Ctrl+Down, then a plain 'Z' to catch any leak
}, func(kr *keyReader) {
k := kr.next()
if k.special != keyDown {
t.Fatalf("got %v, want keyDown", k.special)
}
k2 := kr.next()
if k2.special != keyRune || k2.r != 'Z' {
t.Fatalf("leftover CSI bytes leaked as input: got %+v, want plain rune 'Z'", k2)
}
})
}
// A syntactically valid CSI sequence this app has no key for (F5) must be
// fully consumed and ignored, not misparsed into a leak, and not treated as
// Esc (which would cancel the form for a key that was never meant to).
func TestKeyReaderUnmappedCSIIgnored(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.WriteString("\x1b[15~Z") // F5, then a plain 'Z' to catch any leak
}, func(kr *keyReader) {
k := kr.next()
if k.special != keyNone {
t.Fatalf("got %v, want keyNone (ignored, not a cancel)", k.special)
}
k2 := kr.next()
if k2.special != keyRune || k2.r != 'Z' {
t.Fatalf("leftover CSI bytes leaked as input: got %+v, want plain rune 'Z'", k2)
}
})
}
func TestKeyReaderControlKeys(t *testing.T) {
seqs := map[byte]specialKey{
0x03: keyCtrlC,
0x13: keyCtrlS,
'\r': keyEnter,
'\n': keyEnter,
0x7f: keyBackspace,
0x08: keyBackspace,
0x09: keyTab,
}
for b, want := range seqs {
b, want := b, want
t.Run(string(rune(b)), func(t *testing.T) {
withKeyReader(t, func(w *os.File) {
w.Write([]byte{b})
}, func(kr *keyReader) {
k := kr.next()
if k.special != want {
t.Fatalf("byte %#x: got %v, want %v", b, k.special, want)
}
})
})
}
}
// A lone Esc (not followed by '[') must resolve to keyEsc, not hang forever.
func TestKeyReaderLoneEsc(t *testing.T) {
done := make(chan struct{})
withKeyReader(t, func(w *os.File) {
w.Write([]byte{0x1b})
}, func(kr *keyReader) {
go func() {
k := kr.next()
if k.special != keyEsc {
t.Errorf("got %v, want keyEsc", k.special)
}
close(done)
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for lone Esc to resolve")
}
})
}
// Esc must resolve *promptly*: every Esc press waits out escDelay in full,
// so that constant is felt directly by anyone using the form. Unlike
// TestKeyReaderLoneEsc this deliberately keeps the write end of the pipe
// open, because closing it lets readEscape return via its EOF path without
// ever consulting the timeout - which is exactly what made an earlier
// version of this test pass with escDelay at 600ms.
//
// The ceiling is generous enough not to flake on a loaded CI box, while
// still failing if escDelay creeps back into the multi-hundred-ms range it
// was once (wrongly) raised to - see escDelay's comment in tty.go.
func TestKeyReaderLoneEscIsPrompt(t *testing.T) {
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer w.Close() // stays open across the read, unlike withKeyReader
kr := newKeyReader(r)
if _, err := w.Write([]byte{0x1b}); err != nil {
t.Fatal(err)
}
type result struct {
k key
elapsed time.Duration
}
got := make(chan result, 1)
start := time.Now()
go func() {
k := kr.next()
got <- result{k, time.Since(start)}
}()
select {
case res := <-got:
if res.k.special != keyEsc {
t.Fatalf("got %v, want keyEsc", res.k.special)
}
if res.elapsed < escDelay {
t.Fatalf("resolved in %v, faster than escDelay (%v) - the timeout "+
"path wasn't exercised, so this test proves nothing", res.elapsed, escDelay)
}
if res.elapsed > 250*time.Millisecond {
t.Errorf("lone Esc took %v to resolve - too sluggish to type against", res.elapsed)
}
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for lone Esc to resolve")
}
}
func TestUtf8SeqLen(t *testing.T) {
cases := map[byte]int{
'a': 1,
0xC3: 2,
0xE2: 3,
0xF0: 4,
}
for b, want := range cases {
if got := utf8SeqLen(b); got != want {
t.Errorf("utf8SeqLen(%#x) = %d, want %d", b, got, want)
}
}
}