242 lines
6.2 KiB
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)
|
|
}
|
|
}
|
|
}
|