mirror of
https://github.com/golang/term.git
synced 2026-01-29 07:02:06 +03:00
The behavior implemented here matches readline and libedit. Updates golang/go#76826 Change-Id: I893677f9bceaf75aa1dada7d893845728e07057e Reviewed-on: https://go-review.googlesource.com/c/term/+/730441 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Ian Lance Taylor <iant@golang.org> Reviewed-by: Michael Knyszek <mknyszek@google.com> Reviewed-by: David Chase <drchase@google.com>
593 lines
13 KiB
Go
593 lines
13 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package term
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
type MockTerminal struct {
|
|
toSend []byte
|
|
bytesPerRead int
|
|
received []byte
|
|
}
|
|
|
|
func (c *MockTerminal) Read(data []byte) (n int, err error) {
|
|
n = len(data)
|
|
if n == 0 {
|
|
return
|
|
}
|
|
if n > len(c.toSend) {
|
|
n = len(c.toSend)
|
|
}
|
|
if n == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
if c.bytesPerRead > 0 && n > c.bytesPerRead {
|
|
n = c.bytesPerRead
|
|
}
|
|
copy(data, c.toSend[:n])
|
|
c.toSend = c.toSend[n:]
|
|
return
|
|
}
|
|
|
|
func (c *MockTerminal) Write(data []byte) (n int, err error) {
|
|
c.received = append(c.received, data...)
|
|
return len(data), nil
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
c := &MockTerminal{}
|
|
ss := NewTerminal(c, "> ")
|
|
line, err := ss.ReadLine()
|
|
if line != "" {
|
|
t.Errorf("Expected empty line but got: %s", line)
|
|
}
|
|
if err != io.EOF {
|
|
t.Errorf("Error should have been EOF but got: %s", err)
|
|
}
|
|
}
|
|
|
|
var keyPressTests = []struct {
|
|
in string
|
|
line string
|
|
err error
|
|
throwAwayLines int
|
|
}{
|
|
{
|
|
err: io.EOF,
|
|
},
|
|
{
|
|
in: "\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "foo\r",
|
|
line: "foo",
|
|
},
|
|
{
|
|
in: "a\x1b[Cb\r", // right
|
|
line: "ab",
|
|
},
|
|
{
|
|
in: "a\x1b[Db\r", // left
|
|
line: "ba",
|
|
},
|
|
{
|
|
in: "a\006b\r", // ^F
|
|
line: "ab",
|
|
},
|
|
{
|
|
in: "a\002b\r", // ^B
|
|
line: "ba",
|
|
},
|
|
{
|
|
in: "a\177b\r", // backspace
|
|
line: "b",
|
|
},
|
|
{
|
|
in: "\x1b[A\r", // up
|
|
},
|
|
{
|
|
in: "\x1b[B\r", // down
|
|
},
|
|
{
|
|
in: "\016\r", // ^P
|
|
},
|
|
{
|
|
in: "\014\r", // ^N
|
|
},
|
|
{
|
|
in: "line\x1b[A\x1b[B\r", // up then down
|
|
line: "line",
|
|
},
|
|
{
|
|
in: "line1\rline2\x1b[A\r", // recall previous line.
|
|
line: "line1",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// recall two previous lines and append.
|
|
in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r",
|
|
line: "line1xxx",
|
|
throwAwayLines: 2,
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line followed by ^K to kill
|
|
// line.
|
|
in: "a b \001\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line, Ctrl-E to move to end,
|
|
// finally ^K to kill nothing.
|
|
in: "a b \001\005\013\r",
|
|
line: "a b ",
|
|
},
|
|
{
|
|
in: "\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a \027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a b\027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "a b \027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "one two thr\x1b[D\027\r",
|
|
line: "one two r",
|
|
},
|
|
{
|
|
in: "\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "ab\x1b[D\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "Ξεσκεπάζω\r",
|
|
line: "Ξεσκεπάζω",
|
|
},
|
|
{
|
|
in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace.
|
|
line: "",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter.
|
|
line: "£",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Ctrl-D at the end of the line should be ignored.
|
|
in: "a\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, left, Ctrl-D should erase the b.
|
|
in: "ab\x1b[D\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, c, d, left, left, ^U should erase to the beginning of
|
|
// the line.
|
|
in: "abcd\x1b[D\x1b[D\025\r",
|
|
line: "cd",
|
|
},
|
|
{
|
|
// Bracketed paste mode: control sequences should be returned
|
|
// verbatim in paste mode.
|
|
in: "abc\x1b[200~de\177f\x1b[201~\177\r",
|
|
line: "abcde\177",
|
|
},
|
|
{
|
|
// Enter in bracketed paste mode should still work.
|
|
in: "abc\x1b[200~d\refg\x1b[201~h\r",
|
|
line: "efgh",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Newline in bracketed paste mode should still work.
|
|
in: "abc\x1b[200~d\nefg\x1b[201~h\r",
|
|
line: "efgh",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Lines consisting entirely of pasted data should be indicated as such.
|
|
in: "\x1b[200~a\r",
|
|
line: "a",
|
|
err: ErrPasteIndicator,
|
|
},
|
|
{
|
|
// Lines consisting entirely of pasted data should be indicated as such (\n paste).
|
|
in: "\x1b[200~a\n",
|
|
line: "a",
|
|
err: ErrPasteIndicator,
|
|
},
|
|
{
|
|
// Ctrl-C terminates readline
|
|
in: "\003",
|
|
err: io.EOF,
|
|
},
|
|
{
|
|
// Ctrl-C at the end of line also terminates readline
|
|
in: "a\003\r",
|
|
err: io.EOF,
|
|
},
|
|
{
|
|
// Delete at EOL: nothing
|
|
in: "abc\x1b[3~\r",
|
|
line: "abc",
|
|
},
|
|
{
|
|
// Delete in empty string: nothing
|
|
in: "\x1b[3~\r",
|
|
line: "",
|
|
},
|
|
{
|
|
// Move left, delete: delete 'c'
|
|
in: "abc\x1b[D\x1b[3~\r",
|
|
line: "ab",
|
|
},
|
|
{
|
|
// Home, delete: delete 'a'
|
|
in: "abc\x1b[H\x1b[3~\r",
|
|
line: "bc",
|
|
},
|
|
{
|
|
// Home, delete twice: delete 'a' and 'b'
|
|
in: "abc\x1b[H\x1b[3~\x1b[3~\r",
|
|
line: "c",
|
|
},
|
|
{
|
|
// Ctrl-T at end of line: transpose last two chars
|
|
in: "abc\x14\r",
|
|
line: "acb",
|
|
},
|
|
{
|
|
// Ctrl-T at end then type: cursor stays at end
|
|
in: "abc\x14N\r",
|
|
line: "acbN",
|
|
},
|
|
{
|
|
// Ctrl-T in middle: transpose chars before cursor, move cursor forward
|
|
in: "abc\x1b[D\x14\r",
|
|
line: "acb",
|
|
},
|
|
{
|
|
// Ctrl-T in middle then type: cursor moved past swapped char
|
|
in: "abcd\x1b[D\x1b[D\x14N\r",
|
|
line: "acbNd",
|
|
},
|
|
{
|
|
// Ctrl-T at pos 1 then type: cursor moves to pos 2
|
|
in: "abc\x1b[H\x1b[C\x14N\r",
|
|
line: "baNc",
|
|
},
|
|
{
|
|
// Ctrl-T with one char: do nothing
|
|
in: "a\x14\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// Ctrl-T with one char then type: cursor unchanged
|
|
in: "a\x14N\r",
|
|
line: "aN",
|
|
},
|
|
{
|
|
// Ctrl-T at beginning: do nothing
|
|
in: "ab\x1b[H\x14\r",
|
|
line: "ab",
|
|
},
|
|
{
|
|
// Ctrl-T at beginning then type: cursor unchanged, inserts at start
|
|
in: "ab\x1b[H\x14N\r",
|
|
line: "Nab",
|
|
},
|
|
{
|
|
// Ctrl-T on empty line: do nothing
|
|
in: "\x14\r",
|
|
line: "",
|
|
},
|
|
{
|
|
// Multiple Ctrl-T at end: keeps swapping last two
|
|
in: "abc\x14\x14\r",
|
|
line: "abc",
|
|
},
|
|
{
|
|
// Multiple Ctrl-T in middle: progresses through line
|
|
in: "abcd\x1b[D\x1b[D\x1b[D\x14\x14\x14\r",
|
|
line: "bcda",
|
|
},
|
|
}
|
|
|
|
func TestKeyPresses(t *testing.T) {
|
|
for i, test := range keyPressTests {
|
|
for j := 1; j < len(test.in); j++ {
|
|
c := &MockTerminal{
|
|
toSend: []byte(test.in),
|
|
bytesPerRead: j,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
for k := 0; k < test.throwAwayLines; k++ {
|
|
_, err := ss.ReadLine()
|
|
if err != nil {
|
|
t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err)
|
|
}
|
|
}
|
|
line, err := ss.ReadLine()
|
|
if line != test.line {
|
|
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
|
break
|
|
}
|
|
if err != test.err {
|
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var renderTests = []struct {
|
|
in string
|
|
received string
|
|
err error
|
|
}{
|
|
{
|
|
// Cursor move after keyHome (left 4) then enter (right 4, newline)
|
|
in: "abcd\x1b[H\r",
|
|
received: "> abcd\x1b[4D\x1b[4C\r\n",
|
|
},
|
|
{
|
|
// Write, home, prepend, enter. Prepends rewrites the line.
|
|
in: "cdef\x1b[Hab\r",
|
|
received: "> cdef" + // Initial input
|
|
"\x1b[4Da" + // Move cursor back, insert first char
|
|
"cdef" + // Copy over original string
|
|
"\x1b[4Dbcdef" + // Repeat for second char with copy
|
|
"\x1b[4D" + // Put cursor back in position to insert again
|
|
"\x1b[4C\r\n", // Put cursor at the end of the line and newline.
|
|
},
|
|
}
|
|
|
|
func TestRender(t *testing.T) {
|
|
for i, test := range renderTests {
|
|
for j := 1; j < len(test.in); j++ {
|
|
c := &MockTerminal{
|
|
toSend: []byte(test.in),
|
|
bytesPerRead: j,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
_, err := ss.ReadLine()
|
|
if err != test.err {
|
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
|
break
|
|
}
|
|
if test.received != string(c.received) {
|
|
t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCRLF(t *testing.T) {
|
|
c := &MockTerminal{
|
|
toSend: []byte("line1\rline2\r\nline3\n"),
|
|
// bytesPerRead 0 in this test means read all at once
|
|
// CR+LF need to be in same read for ReadLine to not produce an extra empty line
|
|
// which is what terminals do for reasonably small paste. if way many lines are pasted
|
|
// and going over say 1k-16k buffer, readline current implementation will possibly generate 1
|
|
// extra empty line, if the CR is in chunk1 and LF in chunk2 (and that's fine).
|
|
}
|
|
|
|
ss := NewTerminal(c, "> ")
|
|
for i := range 3 {
|
|
line, err := ss.ReadLine()
|
|
if err != nil {
|
|
t.Fatalf("failed to read line %d: %v", i+1, err)
|
|
}
|
|
expected := fmt.Sprintf("line%d", i+1)
|
|
if line != expected {
|
|
t.Fatalf("expected '%s', got '%s'", expected, line)
|
|
}
|
|
}
|
|
line, err := ss.ReadLine()
|
|
if !errors.Is(err, io.EOF) {
|
|
t.Fatalf("expected EOF after 3 lines, got '%s' with error %v", line, err)
|
|
}
|
|
if line != "" {
|
|
t.Fatalf("expected empty line after EOF, got '%s'", line)
|
|
}
|
|
}
|
|
|
|
func TestPasswordNotSaved(t *testing.T) {
|
|
c := &MockTerminal{
|
|
toSend: []byte("password\r\x1b[A\r"),
|
|
bytesPerRead: 1,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
pw, _ := ss.ReadPassword("> ")
|
|
if pw != "password" {
|
|
t.Fatalf("failed to read password, got %s", pw)
|
|
}
|
|
line, _ := ss.ReadLine()
|
|
if len(line) > 0 {
|
|
t.Fatalf("password was saved in history")
|
|
}
|
|
}
|
|
|
|
var setSizeTests = []struct {
|
|
width, height int
|
|
}{
|
|
{40, 13},
|
|
{80, 24},
|
|
{132, 43},
|
|
}
|
|
|
|
func TestTerminalSetSize(t *testing.T) {
|
|
for _, setSize := range setSizeTests {
|
|
c := &MockTerminal{
|
|
toSend: []byte("password\r\x1b[A\r"),
|
|
bytesPerRead: 1,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
ss.SetSize(setSize.width, setSize.height)
|
|
pw, _ := ss.ReadPassword("Password: ")
|
|
if pw != "password" {
|
|
t.Fatalf("failed to read password, got %s", pw)
|
|
}
|
|
if string(c.received) != "Password: \r\n" {
|
|
t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadPasswordLineEnd(t *testing.T) {
|
|
type testType struct {
|
|
input string
|
|
want string
|
|
}
|
|
tests := []testType{
|
|
{"\r\n", ""},
|
|
{"test\r\n", "test"},
|
|
{"test\r", "test"},
|
|
{"test\n", "test"},
|
|
{"testtesttesttes\n", "testtesttesttes"},
|
|
{"testtesttesttes\r\n", "testtesttesttes"},
|
|
{"testtesttesttesttest\n", "testtesttesttesttest"},
|
|
{"testtesttesttesttest\r\n", "testtesttesttesttest"},
|
|
{"\btest", "test"},
|
|
{"t\best", "est"},
|
|
{"te\bst", "tst"},
|
|
{"test\b", "tes"},
|
|
{"test\b\r\n", "tes"},
|
|
{"test\b\n", "tes"},
|
|
{"test\b\r", "tes"},
|
|
}
|
|
eol := "\n"
|
|
if runtime.GOOS == "windows" {
|
|
eol = "\r"
|
|
}
|
|
tests = append(tests, testType{eol, ""})
|
|
for _, test := range tests {
|
|
buf := new(bytes.Buffer)
|
|
if _, err := buf.WriteString(test.input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
have, err := readPasswordLine(buf)
|
|
if err != nil {
|
|
t.Errorf("readPasswordLine(%q) failed: %v", test.input, err)
|
|
continue
|
|
}
|
|
if string(have) != test.want {
|
|
t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want)
|
|
continue
|
|
}
|
|
|
|
if _, err = buf.WriteString(test.input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
have, err = readPasswordLine(buf)
|
|
if err != nil {
|
|
t.Errorf("readPasswordLine(%q) failed: %v", test.input, err)
|
|
continue
|
|
}
|
|
if string(have) != test.want {
|
|
t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func MockAutoCompleteCallback(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
|
|
return "not-good", pos, true
|
|
}
|
|
|
|
func TestReadPasswordDisabledAutoCompleteCallback(t *testing.T) {
|
|
input := "testgood\ranother line\r"
|
|
expectedPassword := "testgood"
|
|
terminal := NewTerminal(
|
|
&MockTerminal{
|
|
toSend: []byte(input),
|
|
bytesPerRead: 1,
|
|
},
|
|
"prompt")
|
|
terminal.AutoCompleteCallback = MockAutoCompleteCallback
|
|
password, err := terminal.ReadPassword("Password: ")
|
|
if err != nil {
|
|
t.Fatalf("failed to read password: %v", err)
|
|
}
|
|
if password != expectedPassword {
|
|
t.Fatalf("failed to read password, got %q", password)
|
|
}
|
|
if terminal.AutoCompleteCallback == nil {
|
|
t.Fatalf("AutoCompleteCallback should not be nil after ReadPassword")
|
|
}
|
|
}
|
|
|
|
func TestMakeRawState(t *testing.T) {
|
|
fd := int(os.Stdout.Fd())
|
|
if !IsTerminal(fd) {
|
|
t.Skip("stdout is not a terminal; skipping test")
|
|
}
|
|
|
|
st, err := GetState(fd)
|
|
if err != nil {
|
|
t.Fatalf("failed to get terminal state from GetState: %s", err)
|
|
}
|
|
|
|
if runtime.GOOS == "ios" {
|
|
t.Skip("MakeRaw not allowed on iOS; skipping test")
|
|
}
|
|
|
|
defer Restore(fd, st)
|
|
raw, err := MakeRaw(fd)
|
|
if err != nil {
|
|
t.Fatalf("failed to get terminal state from MakeRaw: %s", err)
|
|
}
|
|
|
|
if *st != *raw {
|
|
t.Errorf("states do not match; was %v, expected %v", raw, st)
|
|
}
|
|
}
|
|
|
|
func TestOutputNewlines(t *testing.T) {
|
|
// \n should be changed to \r\n in terminal output.
|
|
buf := new(bytes.Buffer)
|
|
term := NewTerminal(buf, ">")
|
|
|
|
term.Write([]byte("1\n2\n"))
|
|
output := buf.String()
|
|
const expected = "1\r\n2\r\n"
|
|
|
|
if output != expected {
|
|
t.Errorf("incorrect output: was %q, expected %q", output, expected)
|
|
}
|
|
}
|