| // 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" |
| "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, |
| }, |
| { |
| // Lines consisting entirely of pasted data should be indicated as such. |
| in: "\x1b[200~a\r", |
| 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, |
| }, |
| } |
| |
| 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 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 |
| } |
| var 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 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) |
| } |
| } |