Merge pull request #11566 from tiborvass/carry-10864
Carry 10864: ANSI terminal emulation for windows
This commit is contained in:
commit
c9fdd26131
7 changed files with 1939 additions and 102 deletions
|
@ -1,87 +0,0 @@
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package term
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Consts for Get/SetConsoleMode function
|
|
||||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx
|
|
||||||
ENABLE_ECHO_INPUT = 0x0004
|
|
||||||
ENABLE_INSERT_MODE = 0x0020
|
|
||||||
ENABLE_LINE_INPUT = 0x0002
|
|
||||||
ENABLE_MOUSE_INPUT = 0x0010
|
|
||||||
ENABLE_PROCESSED_INPUT = 0x0001
|
|
||||||
ENABLE_QUICK_EDIT_MODE = 0x0040
|
|
||||||
ENABLE_WINDOW_INPUT = 0x0008
|
|
||||||
// If parameter is a screen buffer handle, additional values
|
|
||||||
ENABLE_PROCESSED_OUTPUT = 0x0001
|
|
||||||
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
|
|
||||||
)
|
|
||||||
|
|
||||||
var kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
|
|
||||||
|
|
||||||
var (
|
|
||||||
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
|
|
||||||
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetConsoleMode(fileDesc uintptr) (uint32, error) {
|
|
||||||
var mode uint32
|
|
||||||
err := syscall.GetConsoleMode(syscall.Handle(fileDesc), &mode)
|
|
||||||
return mode, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetConsoleMode(fileDesc uintptr, mode uint32) error {
|
|
||||||
r, _, err := setConsoleModeProc.Call(fileDesc, uintptr(mode), 0)
|
|
||||||
if r == 0 {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return syscall.EINVAL
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// types for calling GetConsoleScreenBufferInfo
|
|
||||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682093(v=vs.85).aspx
|
|
||||||
type (
|
|
||||||
SHORT int16
|
|
||||||
|
|
||||||
SMALL_RECT struct {
|
|
||||||
Left SHORT
|
|
||||||
Top SHORT
|
|
||||||
Right SHORT
|
|
||||||
Bottom SHORT
|
|
||||||
}
|
|
||||||
|
|
||||||
COORD struct {
|
|
||||||
X SHORT
|
|
||||||
Y SHORT
|
|
||||||
}
|
|
||||||
|
|
||||||
WORD uint16
|
|
||||||
|
|
||||||
CONSOLE_SCREEN_BUFFER_INFO struct {
|
|
||||||
dwSize COORD
|
|
||||||
dwCursorPosition COORD
|
|
||||||
wAttributes WORD
|
|
||||||
srWindow SMALL_RECT
|
|
||||||
dwMaximumWindowSize COORD
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetConsoleScreenBufferInfo(fileDesc uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
|
|
||||||
var info CONSOLE_SCREEN_BUFFER_INFO
|
|
||||||
r, _, err := getConsoleScreenBufferInfoProc.Call(uintptr(fileDesc), uintptr(unsafe.Pointer(&info)), 0)
|
|
||||||
if r == 0 {
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, syscall.EINVAL
|
|
||||||
}
|
|
||||||
return &info, nil
|
|
||||||
}
|
|
15
term/term.go
15
term/term.go
|
@ -4,6 +4,7 @@ package term
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
@ -25,6 +26,20 @@ type Winsize struct {
|
||||||
y uint16
|
y uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
|
||||||
|
return os.Stdin, os.Stdout, os.Stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFdInfo(in interface{}) (uintptr, bool) {
|
||||||
|
var inFd uintptr
|
||||||
|
var isTerminalIn bool
|
||||||
|
if file, ok := in.(*os.File); ok {
|
||||||
|
inFd = file.Fd()
|
||||||
|
isTerminalIn = IsTerminal(inFd)
|
||||||
|
}
|
||||||
|
return inFd, isTerminalIn
|
||||||
|
}
|
||||||
|
|
||||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||||
ws := &Winsize{}
|
ws := &Winsize{}
|
||||||
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
|
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(ws)))
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
// +build windows
|
// +build windows
|
||||||
|
|
||||||
package term
|
package term
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/docker/docker/pkg/term/winconsole"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State holds the console mode for the terminal.
|
||||||
type State struct {
|
type State struct {
|
||||||
mode uint32
|
mode uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Winsize is used for window size.
|
||||||
type Winsize struct {
|
type Winsize struct {
|
||||||
Height uint16
|
Height uint16
|
||||||
Width uint16
|
Width uint16
|
||||||
|
@ -13,15 +20,17 @@ type Winsize struct {
|
||||||
y uint16
|
y uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWinsize gets the window size of the given terminal
|
||||||
func GetWinsize(fd uintptr) (*Winsize, error) {
|
func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||||
ws := &Winsize{}
|
ws := &Winsize{}
|
||||||
var info *CONSOLE_SCREEN_BUFFER_INFO
|
var info *winconsole.CONSOLE_SCREEN_BUFFER_INFO
|
||||||
info, err := GetConsoleScreenBufferInfo(fd)
|
info, err := winconsole.GetConsoleScreenBufferInfo(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ws.Height = uint16(info.srWindow.Right - info.srWindow.Left + 1)
|
|
||||||
ws.Width = uint16(info.srWindow.Bottom - info.srWindow.Top + 1)
|
ws.Width = uint16(info.Window.Right - info.Window.Left + 1)
|
||||||
|
ws.Height = uint16(info.Window.Bottom - info.Window.Top + 1)
|
||||||
|
|
||||||
ws.x = 0 // todo azlinux -- this is the pixel size of the Window, and not currently used by any caller
|
ws.x = 0 // todo azlinux -- this is the pixel size of the Window, and not currently used by any caller
|
||||||
ws.y = 0
|
ws.y = 0
|
||||||
|
@ -29,37 +38,44 @@ func GetWinsize(fd uintptr) (*Winsize, error) {
|
||||||
return ws, nil
|
return ws, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetWinsize sets the terminal connected to the given file descriptor to a
|
||||||
|
// given size.
|
||||||
func SetWinsize(fd uintptr, ws *Winsize) error {
|
func SetWinsize(fd uintptr, ws *Winsize) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
func IsTerminal(fd uintptr) bool {
|
func IsTerminal(fd uintptr) bool {
|
||||||
_, e := GetConsoleMode(fd)
|
_, e := winconsole.GetConsoleMode(fd)
|
||||||
return e == nil
|
return e == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore restores the terminal connected to the given file descriptor to a
|
// RestoreTerminal restores the terminal connected to the given file descriptor to a
|
||||||
// previous state.
|
// previous state.
|
||||||
func RestoreTerminal(fd uintptr, state *State) error {
|
func RestoreTerminal(fd uintptr, state *State) error {
|
||||||
return SetConsoleMode(fd, state.mode)
|
return winconsole.SetConsoleMode(fd, state.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SaveState saves the state of the given console
|
||||||
func SaveState(fd uintptr) (*State, error) {
|
func SaveState(fd uintptr) (*State, error) {
|
||||||
mode, e := GetConsoleMode(fd)
|
mode, e := winconsole.GetConsoleMode(fd)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, e
|
return nil, e
|
||||||
}
|
}
|
||||||
return &State{mode}, nil
|
return &State{mode}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DisableEcho disbales the echo for given file descriptor and returns previous state
|
||||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings
|
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings
|
||||||
func DisableEcho(fd uintptr, state *State) error {
|
func DisableEcho(fd uintptr, state *State) error {
|
||||||
state.mode &^= (ENABLE_ECHO_INPUT)
|
state.mode &^= (winconsole.ENABLE_ECHO_INPUT)
|
||||||
state.mode |= (ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT)
|
state.mode |= (winconsole.ENABLE_PROCESSED_INPUT | winconsole.ENABLE_LINE_INPUT)
|
||||||
return SetConsoleMode(fd, state.mode)
|
return winconsole.SetConsoleMode(fd, state.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRawTerminal puts the terminal connected to the given file descriptor into raw
|
||||||
|
// mode and returns the previous state of the terminal so that it can be
|
||||||
|
// restored.
|
||||||
func SetRawTerminal(fd uintptr) (*State, error) {
|
func SetRawTerminal(fd uintptr) (*State, error) {
|
||||||
oldState, err := MakeRaw(fd)
|
oldState, err := MakeRaw(fd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -79,11 +95,24 @@ func MakeRaw(fd uintptr) (*State, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx for these flag settings
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms683462(v=vs.85).aspx
|
||||||
state.mode &^= (ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT | ENABLE_LINE_INPUT)
|
// All three input modes, along with processed output mode, are designed to work together.
|
||||||
err = SetConsoleMode(fd, state.mode)
|
// It is best to either enable or disable all of these modes as a group.
|
||||||
|
// When all are enabled, the application is said to be in "cooked" mode, which means that most of the processing is handled for the application.
|
||||||
|
// When all are disabled, the application is in "raw" mode, which means that input is unfiltered and any processing is left to the application.
|
||||||
|
state.mode = 0
|
||||||
|
err = winconsole.SetConsoleMode(fd, state.mode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetFdInfo returns file descriptor and bool indicating whether the file is a terminal
|
||||||
|
func GetFdInfo(in interface{}) (uintptr, bool) {
|
||||||
|
return winconsole.GetHandleInfo(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StdStreams() (stdIn io.ReadCloser, stdOut, stdErr io.Writer) {
|
||||||
|
return winconsole.StdStreams()
|
||||||
|
}
|
||||||
|
|
1042
term/winconsole/console_windows.go
Normal file
1042
term/winconsole/console_windows.go
Normal file
File diff suppressed because it is too large
Load diff
232
term/winconsole/console_windows_test.go
Normal file
232
term/winconsole/console_windows_test.go
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winconsole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func helpsTestParseInt16OrDefault(t *testing.T, expectedValue int16, shouldFail bool, input string, defaultValue int16, format string, args ...string) {
|
||||||
|
value, err := parseInt16OrDefault(input, defaultValue)
|
||||||
|
if nil != err && !shouldFail {
|
||||||
|
t.Errorf("Unexpected error returned %v", err)
|
||||||
|
t.Errorf(format, args)
|
||||||
|
}
|
||||||
|
if nil == err && shouldFail {
|
||||||
|
t.Errorf("Should have failed as expected\n\tReturned value = %d", value)
|
||||||
|
t.Errorf(format, args)
|
||||||
|
}
|
||||||
|
if expectedValue != value {
|
||||||
|
t.Errorf("The value returned does not macth expected\n\tExpected:%v\n\t:Actual%v", expectedValue, value)
|
||||||
|
t.Errorf(format, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseInt16OrDefault(t *testing.T) {
|
||||||
|
// empty string
|
||||||
|
helpsTestParseInt16OrDefault(t, 0, false, "", 0, "Empty string returns default")
|
||||||
|
helpsTestParseInt16OrDefault(t, 2, false, "", 2, "Empty string returns default")
|
||||||
|
|
||||||
|
// normal case
|
||||||
|
helpsTestParseInt16OrDefault(t, 0, false, "0", 0, "0 handled correctly")
|
||||||
|
helpsTestParseInt16OrDefault(t, 111, false, "111", 2, "Normal")
|
||||||
|
helpsTestParseInt16OrDefault(t, 111, false, "+111", 2, "+N")
|
||||||
|
helpsTestParseInt16OrDefault(t, -111, false, "-111", 2, "-N")
|
||||||
|
helpsTestParseInt16OrDefault(t, 0, false, "+0", 11, "+0")
|
||||||
|
helpsTestParseInt16OrDefault(t, 0, false, "-0", 12, "-0")
|
||||||
|
|
||||||
|
// ill formed strings
|
||||||
|
helpsTestParseInt16OrDefault(t, 0, true, "abc", 0, "Invalid string")
|
||||||
|
helpsTestParseInt16OrDefault(t, 42, true, "+= 23", 42, "Invalid string")
|
||||||
|
helpsTestParseInt16OrDefault(t, 42, true, "123.45", 42, "float like")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpsTestGetNumberOfChars(t *testing.T, expected uint32, fromCoord COORD, toCoord COORD, screenSize COORD, format string, args ...interface{}) {
|
||||||
|
actual := getNumberOfChars(fromCoord, toCoord, screenSize)
|
||||||
|
mesg := fmt.Sprintf(format, args)
|
||||||
|
assertTrue(t, expected == actual, fmt.Sprintf("%s Expected=%d, Actual=%d, Parameters = { fromCoord=%+v, toCoord=%+v, screenSize=%+v", mesg, expected, actual, fromCoord, toCoord, screenSize))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetNumberOfChars(t *testing.T) {
|
||||||
|
// Note: The columns and lines are 0 based
|
||||||
|
// Also that interval is "inclusive" means will have both start and end chars
|
||||||
|
// This test only tests the number opf characters being written
|
||||||
|
|
||||||
|
// all four corners
|
||||||
|
maxWindow := COORD{X: 80, Y: 50}
|
||||||
|
leftTop := COORD{X: 0, Y: 0}
|
||||||
|
rightTop := COORD{X: 79, Y: 0}
|
||||||
|
leftBottom := COORD{X: 0, Y: 49}
|
||||||
|
rightBottom := COORD{X: 79, Y: 49}
|
||||||
|
|
||||||
|
// same position
|
||||||
|
helpsTestGetNumberOfChars(t, 1, COORD{X: 1, Y: 14}, COORD{X: 1, Y: 14}, COORD{X: 80, Y: 50}, "Same position random line")
|
||||||
|
|
||||||
|
// four corners
|
||||||
|
helpsTestGetNumberOfChars(t, 1, leftTop, leftTop, maxWindow, "Same position- leftTop")
|
||||||
|
helpsTestGetNumberOfChars(t, 1, rightTop, rightTop, maxWindow, "Same position- rightTop")
|
||||||
|
helpsTestGetNumberOfChars(t, 1, leftBottom, leftBottom, maxWindow, "Same position- leftBottom")
|
||||||
|
helpsTestGetNumberOfChars(t, 1, rightBottom, rightBottom, maxWindow, "Same position- rightBottom")
|
||||||
|
|
||||||
|
// from this char to next char on same line
|
||||||
|
helpsTestGetNumberOfChars(t, 2, COORD{X: 0, Y: 0}, COORD{X: 1, Y: 0}, maxWindow, "Next position on same line")
|
||||||
|
helpsTestGetNumberOfChars(t, 2, COORD{X: 1, Y: 14}, COORD{X: 2, Y: 14}, maxWindow, "Next position on same line")
|
||||||
|
|
||||||
|
// from this char to next 10 chars on same line
|
||||||
|
helpsTestGetNumberOfChars(t, 11, COORD{X: 0, Y: 0}, COORD{X: 10, Y: 0}, maxWindow, "Next position on same line")
|
||||||
|
helpsTestGetNumberOfChars(t, 11, COORD{X: 1, Y: 14}, COORD{X: 11, Y: 14}, maxWindow, "Next position on same line")
|
||||||
|
|
||||||
|
helpsTestGetNumberOfChars(t, 5, COORD{X: 3, Y: 11}, COORD{X: 7, Y: 11}, maxWindow, "To and from on same line")
|
||||||
|
|
||||||
|
helpsTestGetNumberOfChars(t, 8, COORD{X: 0, Y: 34}, COORD{X: 7, Y: 34}, maxWindow, "Start of line to middle")
|
||||||
|
helpsTestGetNumberOfChars(t, 4, COORD{X: 76, Y: 34}, COORD{X: 79, Y: 34}, maxWindow, "Middle to end of line")
|
||||||
|
|
||||||
|
// multiple lines - 1
|
||||||
|
helpsTestGetNumberOfChars(t, 81, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 1}, maxWindow, "one line below same X")
|
||||||
|
helpsTestGetNumberOfChars(t, 81, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 11}, maxWindow, "one line below same X")
|
||||||
|
|
||||||
|
// multiple lines - 2
|
||||||
|
helpsTestGetNumberOfChars(t, 161, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 2}, maxWindow, "one line below same X")
|
||||||
|
helpsTestGetNumberOfChars(t, 161, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 12}, maxWindow, "one line below same X")
|
||||||
|
|
||||||
|
// multiple lines - 3
|
||||||
|
helpsTestGetNumberOfChars(t, 241, COORD{X: 0, Y: 0}, COORD{X: 0, Y: 3}, maxWindow, "one line below same X")
|
||||||
|
helpsTestGetNumberOfChars(t, 241, COORD{X: 10, Y: 10}, COORD{X: 10, Y: 13}, maxWindow, "one line below same X")
|
||||||
|
|
||||||
|
// full line
|
||||||
|
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 0}, COORD{X: 79, Y: 0}, maxWindow, "Full line - first")
|
||||||
|
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 23}, COORD{X: 79, Y: 23}, maxWindow, "Full line - random")
|
||||||
|
helpsTestGetNumberOfChars(t, 80, COORD{X: 0, Y: 49}, COORD{X: 79, Y: 49}, maxWindow, "Full line - last")
|
||||||
|
|
||||||
|
// full screen
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50, leftTop, rightBottom, maxWindow, "full screen")
|
||||||
|
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-1, COORD{X: 1, Y: 0}, rightBottom, maxWindow, "dropping first char to, end of screen")
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-2, COORD{X: 2, Y: 0}, rightBottom, maxWindow, "dropping first two char to, end of screen")
|
||||||
|
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-1, leftTop, COORD{X: 78, Y: 49}, maxWindow, "from start of screen, till last char-1")
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-2, leftTop, COORD{X: 77, Y: 49}, maxWindow, "from start of screen, till last char-2")
|
||||||
|
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-5, COORD{X: 4, Y: 0}, COORD{X: 78, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-1")
|
||||||
|
helpsTestGetNumberOfChars(t, 80*50-6, COORD{X: 4, Y: 0}, COORD{X: 77, Y: 49}, COORD{X: 80, Y: 50}, "from start of screen+4, till last char-2")
|
||||||
|
}
|
||||||
|
|
||||||
|
var allForeground = []int16{
|
||||||
|
ANSI_FOREGROUND_BLACK,
|
||||||
|
ANSI_FOREGROUND_RED,
|
||||||
|
ANSI_FOREGROUND_GREEN,
|
||||||
|
ANSI_FOREGROUND_YELLOW,
|
||||||
|
ANSI_FOREGROUND_BLUE,
|
||||||
|
ANSI_FOREGROUND_MAGENTA,
|
||||||
|
ANSI_FOREGROUND_CYAN,
|
||||||
|
ANSI_FOREGROUND_WHITE,
|
||||||
|
ANSI_FOREGROUND_DEFAULT,
|
||||||
|
}
|
||||||
|
var allBackground = []int16{
|
||||||
|
ANSI_BACKGROUND_BLACK,
|
||||||
|
ANSI_BACKGROUND_RED,
|
||||||
|
ANSI_BACKGROUND_GREEN,
|
||||||
|
ANSI_BACKGROUND_YELLOW,
|
||||||
|
ANSI_BACKGROUND_BLUE,
|
||||||
|
ANSI_BACKGROUND_MAGENTA,
|
||||||
|
ANSI_BACKGROUND_CYAN,
|
||||||
|
ANSI_BACKGROUND_WHITE,
|
||||||
|
ANSI_BACKGROUND_DEFAULT,
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskForeground(flag WORD) WORD {
|
||||||
|
return flag & FOREGROUND_MASK_UNSET
|
||||||
|
}
|
||||||
|
|
||||||
|
func onlyForeground(flag WORD) WORD {
|
||||||
|
return flag & FOREGROUND_MASK_SET
|
||||||
|
}
|
||||||
|
|
||||||
|
func maskBackground(flag WORD) WORD {
|
||||||
|
return flag & BACKGROUND_MASK_UNSET
|
||||||
|
}
|
||||||
|
|
||||||
|
func onlyBackground(flag WORD) WORD {
|
||||||
|
return flag & BACKGROUND_MASK_SET
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpsTestGetWindowsTextAttributeForAnsiValue(t *testing.T, oldValue WORD /*, expected WORD*/, ansi int16, onlyMask WORD, restMask WORD) WORD {
|
||||||
|
actual, err := getWindowsTextAttributeForAnsiValue(oldValue, FOREGROUND_MASK_SET, ansi)
|
||||||
|
assertTrue(t, nil == err, "Should be no error")
|
||||||
|
// assert that other bits are not affected
|
||||||
|
if 0 != oldValue {
|
||||||
|
assertTrue(t, (actual&restMask) == (oldValue&restMask), "The operation should not have affected other bits actual=%X oldValue=%X ansi=%d", actual, oldValue, ansi)
|
||||||
|
}
|
||||||
|
return actual
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackgroundForAnsiValue(t *testing.T) {
|
||||||
|
// Check that nothing else changes
|
||||||
|
// background changes
|
||||||
|
for _, state1 := range allBackground {
|
||||||
|
for _, state2 := range allBackground {
|
||||||
|
flag := WORD(0)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// cummulative bcakground changes
|
||||||
|
for _, state1 := range allBackground {
|
||||||
|
flag := WORD(0)
|
||||||
|
for _, state2 := range allBackground {
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// change background after foreground
|
||||||
|
for _, state1 := range allForeground {
|
||||||
|
for _, state2 := range allBackground {
|
||||||
|
flag := WORD(0)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// change background after change cumulative
|
||||||
|
for _, state1 := range allForeground {
|
||||||
|
flag := WORD(0)
|
||||||
|
for _, state2 := range allBackground {
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForegroundForAnsiValue(t *testing.T) {
|
||||||
|
// Check that nothing else changes
|
||||||
|
for _, state1 := range allForeground {
|
||||||
|
for _, state2 := range allForeground {
|
||||||
|
flag := WORD(0)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, state1 := range allForeground {
|
||||||
|
flag := WORD(0)
|
||||||
|
for _, state2 := range allForeground {
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, state1 := range allBackground {
|
||||||
|
for _, state2 := range allForeground {
|
||||||
|
flag := WORD(0)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, state1 := range allBackground {
|
||||||
|
flag := WORD(0)
|
||||||
|
for _, state2 := range allForeground {
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state1, BACKGROUND_MASK_SET, BACKGROUND_MASK_UNSET)
|
||||||
|
flag = helpsTestGetWindowsTextAttributeForAnsiValue(t, flag, state2, FOREGROUND_MASK_SET, FOREGROUND_MASK_UNSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
218
term/winconsole/term_emulator.go
Normal file
218
term/winconsole/term_emulator.go
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
package winconsole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||||
|
const (
|
||||||
|
ANSI_ESCAPE_PRIMARY = 0x1B
|
||||||
|
ANSI_ESCAPE_SECONDARY = 0x5B
|
||||||
|
ANSI_COMMAND_FIRST = 0x40
|
||||||
|
ANSI_COMMAND_LAST = 0x7E
|
||||||
|
ANSI_PARAMETER_SEP = ";"
|
||||||
|
ANSI_CMD_G0 = '('
|
||||||
|
ANSI_CMD_G1 = ')'
|
||||||
|
ANSI_CMD_G2 = '*'
|
||||||
|
ANSI_CMD_G3 = '+'
|
||||||
|
ANSI_CMD_DECPNM = '>'
|
||||||
|
ANSI_CMD_DECPAM = '='
|
||||||
|
ANSI_CMD_OSC = ']'
|
||||||
|
ANSI_CMD_STR_TERM = '\\'
|
||||||
|
ANSI_BEL = 0x07
|
||||||
|
KEY_EVENT = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Interface that implements terminal handling
|
||||||
|
type terminalEmulator interface {
|
||||||
|
HandleOutputCommand(fd uintptr, command []byte) (n int, err error)
|
||||||
|
HandleInputSequence(fd uintptr, command []byte) (n int, err error)
|
||||||
|
WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error)
|
||||||
|
ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type terminalWriter struct {
|
||||||
|
wrappedWriter io.Writer
|
||||||
|
emulator terminalEmulator
|
||||||
|
command []byte
|
||||||
|
inSequence bool
|
||||||
|
fd uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type terminalReader struct {
|
||||||
|
wrappedReader io.ReadCloser
|
||||||
|
emulator terminalEmulator
|
||||||
|
command []byte
|
||||||
|
inSequence bool
|
||||||
|
fd uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
|
||||||
|
func isAnsiCommandChar(b byte) bool {
|
||||||
|
switch {
|
||||||
|
case ANSI_COMMAND_FIRST <= b && b <= ANSI_COMMAND_LAST && b != ANSI_ESCAPE_SECONDARY:
|
||||||
|
return true
|
||||||
|
case b == ANSI_CMD_G1 || b == ANSI_CMD_OSC || b == ANSI_CMD_DECPAM || b == ANSI_CMD_DECPNM:
|
||||||
|
// non-CSI escape sequence terminator
|
||||||
|
return true
|
||||||
|
case b == ANSI_CMD_STR_TERM || b == ANSI_BEL:
|
||||||
|
// String escape sequence terminator
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCharacterSelectionCmdChar(b byte) bool {
|
||||||
|
return (b == ANSI_CMD_G0 || b == ANSI_CMD_G1 || b == ANSI_CMD_G2 || b == ANSI_CMD_G3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isXtermOscSequence(command []byte, current byte) bool {
|
||||||
|
return (len(command) >= 2 && command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_CMD_OSC && current != ANSI_BEL)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write writes len(p) bytes from p to the underlying data stream.
|
||||||
|
// http://golang.org/pkg/io/#Writer
|
||||||
|
func (tw *terminalWriter) Write(p []byte) (n int, err error) {
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if tw.emulator == nil {
|
||||||
|
return tw.wrappedWriter.Write(p)
|
||||||
|
}
|
||||||
|
// Emulate terminal by extracting commands and executing them
|
||||||
|
totalWritten := 0
|
||||||
|
start := 0 // indicates start of the next chunk
|
||||||
|
end := len(p)
|
||||||
|
for current := 0; current < end; current++ {
|
||||||
|
if tw.inSequence {
|
||||||
|
// inside escape sequence
|
||||||
|
tw.command = append(tw.command, p[current])
|
||||||
|
if isAnsiCommandChar(p[current]) {
|
||||||
|
if !isXtermOscSequence(tw.command, p[current]) {
|
||||||
|
// found the last command character.
|
||||||
|
// Now we have a complete command.
|
||||||
|
nchar, err := tw.emulator.HandleOutputCommand(tw.fd, tw.command)
|
||||||
|
totalWritten += nchar
|
||||||
|
if err != nil {
|
||||||
|
return totalWritten, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the command
|
||||||
|
// don't include current character again
|
||||||
|
tw.command = tw.command[:0]
|
||||||
|
start = current + 1
|
||||||
|
tw.inSequence = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if p[current] == ANSI_ESCAPE_PRIMARY {
|
||||||
|
// entering escape sequnce
|
||||||
|
tw.inSequence = true
|
||||||
|
// indicates end of "normal sequence", write whatever you have so far
|
||||||
|
if len(p[start:current]) > 0 {
|
||||||
|
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:current])
|
||||||
|
totalWritten += nw
|
||||||
|
if err != nil {
|
||||||
|
return totalWritten, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// include the current character as part of the next sequence
|
||||||
|
tw.command = append(tw.command, p[current])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// note that so far, start of the escape sequence triggers writing out of bytes to console.
|
||||||
|
// For the part _after_ the end of last escape sequence, it is not written out yet. So write it out
|
||||||
|
if !tw.inSequence {
|
||||||
|
// assumption is that we can't be inside sequence and therefore command should be empty
|
||||||
|
if len(p[start:]) > 0 {
|
||||||
|
nw, err := tw.emulator.WriteChars(tw.fd, tw.wrappedWriter, p[start:])
|
||||||
|
totalWritten += nw
|
||||||
|
if err != nil {
|
||||||
|
return totalWritten, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return totalWritten, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read reads up to len(p) bytes into p.
|
||||||
|
// http://golang.org/pkg/io/#Reader
|
||||||
|
func (tr *terminalReader) Read(p []byte) (n int, err error) {
|
||||||
|
//Implementations of Read are discouraged from returning a zero byte count
|
||||||
|
// with a nil error, except when len(p) == 0.
|
||||||
|
if len(p) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if nil == tr.emulator {
|
||||||
|
return tr.readFromWrappedReader(p)
|
||||||
|
}
|
||||||
|
return tr.emulator.ReadChars(tr.fd, tr.wrappedReader, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the underlying stream
|
||||||
|
func (tr *terminalReader) Close() (err error) {
|
||||||
|
return tr.wrappedReader.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *terminalReader) readFromWrappedReader(p []byte) (n int, err error) {
|
||||||
|
return tr.wrappedReader.Read(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ansiCommand struct {
|
||||||
|
CommandBytes []byte
|
||||||
|
Command string
|
||||||
|
Parameters []string
|
||||||
|
IsSpecial bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseAnsiCommand(command []byte) *ansiCommand {
|
||||||
|
if isCharacterSelectionCmdChar(command[1]) {
|
||||||
|
// Is Character Set Selection commands
|
||||||
|
return &ansiCommand{
|
||||||
|
CommandBytes: command,
|
||||||
|
Command: string(command),
|
||||||
|
IsSpecial: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// last char is command character
|
||||||
|
lastCharIndex := len(command) - 1
|
||||||
|
|
||||||
|
retValue := &ansiCommand{
|
||||||
|
CommandBytes: command,
|
||||||
|
Command: string(command[lastCharIndex]),
|
||||||
|
IsSpecial: false,
|
||||||
|
}
|
||||||
|
// more than a single escape
|
||||||
|
if lastCharIndex != 0 {
|
||||||
|
start := 1
|
||||||
|
// skip if double char escape sequence
|
||||||
|
if command[0] == ANSI_ESCAPE_PRIMARY && command[1] == ANSI_ESCAPE_SECONDARY {
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
// convert this to GetNextParam method
|
||||||
|
retValue.Parameters = strings.Split(string(command[start:lastCharIndex]), ANSI_PARAMETER_SEP)
|
||||||
|
}
|
||||||
|
return retValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ansiCommand) getParam(index int) string {
|
||||||
|
if len(c.Parameters) > index {
|
||||||
|
return c.Parameters[index]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt16OrDefault(s string, defaultValue int16) (n int16, err error) {
|
||||||
|
if s == "" {
|
||||||
|
return defaultValue, nil
|
||||||
|
}
|
||||||
|
parsedValue, err := strconv.ParseInt(s, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue, err
|
||||||
|
}
|
||||||
|
return int16(parsedValue), nil
|
||||||
|
}
|
388
term/winconsole/term_emulator_test.go
Normal file
388
term/winconsole/term_emulator_test.go
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
package winconsole
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WRITE_OPERATION = iota
|
||||||
|
COMMAND_OPERATION = iota
|
||||||
|
)
|
||||||
|
|
||||||
|
var languages = []string{
|
||||||
|
"Български",
|
||||||
|
"Català",
|
||||||
|
"Čeština",
|
||||||
|
"Ελληνικά",
|
||||||
|
"Español",
|
||||||
|
"Esperanto",
|
||||||
|
"Euskara",
|
||||||
|
"Français",
|
||||||
|
"Galego",
|
||||||
|
"한국어",
|
||||||
|
"ქართული",
|
||||||
|
"Latviešu",
|
||||||
|
"Lietuvių",
|
||||||
|
"Magyar",
|
||||||
|
"Nederlands",
|
||||||
|
"日本語",
|
||||||
|
"Norsk bokmål",
|
||||||
|
"Norsk nynorsk",
|
||||||
|
"Polski",
|
||||||
|
"Português",
|
||||||
|
"Română",
|
||||||
|
"Русский",
|
||||||
|
"Slovenčina",
|
||||||
|
"Slovenščina",
|
||||||
|
"Српски",
|
||||||
|
"српскохрватски",
|
||||||
|
"Suomi",
|
||||||
|
"Svenska",
|
||||||
|
"ไทย",
|
||||||
|
"Tiếng Việt",
|
||||||
|
"Türkçe",
|
||||||
|
"Українська",
|
||||||
|
"中文",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock terminal handler object
|
||||||
|
type mockTerminal struct {
|
||||||
|
OutputCommandSequence []terminalOperation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for recording the callback data
|
||||||
|
type terminalOperation struct {
|
||||||
|
Operation int
|
||||||
|
Data []byte
|
||||||
|
Str string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *mockTerminal) record(operation int, data []byte) {
|
||||||
|
op := terminalOperation{
|
||||||
|
Operation: operation,
|
||||||
|
Data: make([]byte, len(data)),
|
||||||
|
}
|
||||||
|
copy(op.Data, data)
|
||||||
|
op.Str = string(op.Data)
|
||||||
|
mt.OutputCommandSequence = append(mt.OutputCommandSequence, op)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *mockTerminal) HandleOutputCommand(fd uintptr, command []byte) (n int, err error) {
|
||||||
|
mt.record(COMMAND_OPERATION, command)
|
||||||
|
return len(command), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *mockTerminal) HandleInputSequence(fd uintptr, command []byte) (n int, err error) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *mockTerminal) WriteChars(fd uintptr, w io.Writer, p []byte) (n int, err error) {
|
||||||
|
mt.record(WRITE_OPERATION, p)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mt *mockTerminal) ReadChars(fd uintptr, w io.Reader, p []byte) (n int, err error) {
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertTrue(t *testing.T, cond bool, format string, args ...interface{}) {
|
||||||
|
if !cond {
|
||||||
|
t.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reflect.DeepEqual does not provide detailed information as to what excatly failed.
|
||||||
|
func assertBytesEqual(t *testing.T, expected, actual []byte, format string, args ...interface{}) {
|
||||||
|
match := true
|
||||||
|
mismatchIndex := 0
|
||||||
|
if len(expected) == len(actual) {
|
||||||
|
for i := 0; i < len(expected); i++ {
|
||||||
|
if expected[i] != actual[i] {
|
||||||
|
match = false
|
||||||
|
mismatchIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match = false
|
||||||
|
t.Errorf("Lengths don't match Expected=%d Actual=%d", len(expected), len(actual))
|
||||||
|
}
|
||||||
|
if !match {
|
||||||
|
t.Errorf("Mismatch at index %d ", mismatchIndex)
|
||||||
|
t.Errorf("\tActual String = %s", string(actual))
|
||||||
|
t.Errorf("\tExpected String = %s", string(expected))
|
||||||
|
t.Errorf("\tActual = %v", actual)
|
||||||
|
t.Errorf("\tExpected = %v", expected)
|
||||||
|
t.Errorf(format, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just to make sure :)
|
||||||
|
func TestAssertEqualBytes(t *testing.T) {
|
||||||
|
data := []byte{9, 9, 1, 1, 1, 9, 9}
|
||||||
|
assertBytesEqual(t, data, data, "Self")
|
||||||
|
assertBytesEqual(t, data[1:4], data[1:4], "Self")
|
||||||
|
assertBytesEqual(t, []byte{1, 1}, []byte{1, 1}, "Simple match")
|
||||||
|
assertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 2, 3}, "content mismatch")
|
||||||
|
assertBytesEqual(t, []byte{1, 1, 1}, data[2:5], "slice match")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func TestAssertEqualBytesNegative(t *testing.T) {
|
||||||
|
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
||||||
|
AssertBytesEqual(t, []byte{1, 1}, []byte{1}, "Length mismatch")
|
||||||
|
AssertBytesEqual(t, []byte{1, 2, 3}, []byte{1, 1, 1}, "content mismatch")
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// Checks that the calls recieved
|
||||||
|
func assertHandlerOutput(t *testing.T, mock *mockTerminal, plainText string, commands ...string) {
|
||||||
|
text := make([]byte, 0, 3*len(plainText))
|
||||||
|
cmdIndex := 0
|
||||||
|
for opIndex := 0; opIndex < len(mock.OutputCommandSequence); opIndex++ {
|
||||||
|
op := mock.OutputCommandSequence[opIndex]
|
||||||
|
if op.Operation == WRITE_OPERATION {
|
||||||
|
t.Logf("\nThe data is[%d] == %s", opIndex, string(op.Data))
|
||||||
|
text = append(text[:], op.Data...)
|
||||||
|
} else {
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[opIndex].Operation == COMMAND_OPERATION, "Operation should be command : %s", fmt.Sprintf("%+v", mock))
|
||||||
|
assertBytesEqual(t, StringToBytes(commands[cmdIndex]), mock.OutputCommandSequence[opIndex].Data, "Command data should match")
|
||||||
|
cmdIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertBytesEqual(t, StringToBytes(plainText), text, "Command data should match %#v", mock)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringToBytes(str string) []byte {
|
||||||
|
bytes := make([]byte, len(str))
|
||||||
|
copy(bytes[:], str)
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAnsiCommand(t *testing.T) {
|
||||||
|
// Note: if the parameter does not exist then the empty value is returned
|
||||||
|
|
||||||
|
c := parseAnsiCommand(StringToBytes("\x1Bm"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||||
|
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||||
|
|
||||||
|
// Escape sequence - ESC[
|
||||||
|
c = parseAnsiCommand(StringToBytes("\x1B[m"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||||
|
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||||
|
|
||||||
|
// Escape sequence With empty parameters- ESC[
|
||||||
|
c = parseAnsiCommand(StringToBytes("\x1B[;m"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "" == c.getParam(0), "should return empty string")
|
||||||
|
assertTrue(t, "" == c.getParam(1), "should return empty string")
|
||||||
|
assertTrue(t, "" == c.getParam(2), "should return empty string")
|
||||||
|
|
||||||
|
// Escape sequence With empty muliple parameters- ESC[
|
||||||
|
c = parseAnsiCommand(StringToBytes("\x1B[;;m"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "" == c.getParam(0), "")
|
||||||
|
assertTrue(t, "" == c.getParam(1), "")
|
||||||
|
assertTrue(t, "" == c.getParam(2), "")
|
||||||
|
|
||||||
|
// Escape sequence With muliple parameters- ESC[
|
||||||
|
c = parseAnsiCommand(StringToBytes("\x1B[1;2;3m"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "1" == c.getParam(0), "")
|
||||||
|
assertTrue(t, "2" == c.getParam(1), "")
|
||||||
|
assertTrue(t, "3" == c.getParam(2), "")
|
||||||
|
|
||||||
|
// Escape sequence With muliple parameters- some missing
|
||||||
|
c = parseAnsiCommand(StringToBytes("\x1B[1;;3;;;6m"))
|
||||||
|
assertTrue(t, c.Command == "m", "Command should be m")
|
||||||
|
assertTrue(t, "1" == c.getParam(0), "")
|
||||||
|
assertTrue(t, "" == c.getParam(1), "")
|
||||||
|
assertTrue(t, "3" == c.getParam(2), "")
|
||||||
|
assertTrue(t, "" == c.getParam(3), "")
|
||||||
|
assertTrue(t, "" == c.getParam(4), "")
|
||||||
|
assertTrue(t, "6" == c.getParam(5), "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBufferedMockTerm() (stdOut io.Writer, stdErr io.Writer, stdIn io.ReadCloser, mock *mockTerminal) {
|
||||||
|
var input bytes.Buffer
|
||||||
|
var output bytes.Buffer
|
||||||
|
var err bytes.Buffer
|
||||||
|
|
||||||
|
mock = &mockTerminal{
|
||||||
|
OutputCommandSequence: make([]terminalOperation, 0, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
stdOut = &terminalWriter{
|
||||||
|
wrappedWriter: &output,
|
||||||
|
emulator: mock,
|
||||||
|
command: make([]byte, 0, 256),
|
||||||
|
}
|
||||||
|
stdErr = &terminalWriter{
|
||||||
|
wrappedWriter: &err,
|
||||||
|
emulator: mock,
|
||||||
|
command: make([]byte, 0, 256),
|
||||||
|
}
|
||||||
|
stdIn = &terminalReader{
|
||||||
|
wrappedReader: ioutil.NopCloser(&input),
|
||||||
|
emulator: mock,
|
||||||
|
command: make([]byte, 0, 256),
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSimple(t *testing.T) {
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
stdOut.Write(StringToBytes("Hello world"))
|
||||||
|
stdOut.Write(StringToBytes("\x1BmHello again"))
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("\x1Bm"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSplitCommand(t *testing.T) {
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
stdOut.Write(StringToBytes("Hello world\x1B[1;2;3"))
|
||||||
|
stdOut.Write(StringToBytes("mHello again"))
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputMultipleCommands(t *testing.T) {
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
stdOut.Write(StringToBytes("Hello world"))
|
||||||
|
stdOut.Write(StringToBytes("\x1B[1;2;3m"))
|
||||||
|
stdOut.Write(StringToBytes("\x1B[J"))
|
||||||
|
stdOut.Write(StringToBytes("Hello again"))
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello world"), mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[1].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("\x1B[1;2;3m"), mock.OutputCommandSequence[1].Data, "Command data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[2].Operation == COMMAND_OPERATION, "Operation should be command : %+v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("\x1B[J"), mock.OutputCommandSequence[2].Data, "Command data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[3].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, StringToBytes("Hello again"), mock.OutputCommandSequence[3].Data, "Write data should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits the given data in two chunks , makes two writes and checks the split data is parsed correctly
|
||||||
|
// checks output write/command is passed to handler correctly
|
||||||
|
func helpsTestOutputSplitChunksAtIndex(t *testing.T, i int, data []byte) {
|
||||||
|
t.Logf("\ni=%d", i)
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
||||||
|
t.Logf("\nWriting chunk[1] == %s", string(data[i:]))
|
||||||
|
stdOut.Write(data[:i])
|
||||||
|
stdOut.Write(data[i:])
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, data[i:], mock.OutputCommandSequence[1].Data, "Write data should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits the given data in three chunks , makes three writes and checks the split data is parsed correctly
|
||||||
|
// checks output write/command is passed to handler correctly
|
||||||
|
func helpsTestOutputSplitThreeChunksAtIndex(t *testing.T, data []byte, i int, j int) {
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
t.Logf("\nWriting chunk[0] == %s", string(data[:i]))
|
||||||
|
t.Logf("\nWriting chunk[1] == %s", string(data[i:j]))
|
||||||
|
t.Logf("\nWriting chunk[2] == %s", string(data[j:]))
|
||||||
|
stdOut.Write(data[:i])
|
||||||
|
stdOut.Write(data[i:j])
|
||||||
|
stdOut.Write(data[j:])
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[0].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, data[:i], mock.OutputCommandSequence[0].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[1].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, data[i:j], mock.OutputCommandSequence[1].Data, "Write data should match")
|
||||||
|
|
||||||
|
assertTrue(t, mock.OutputCommandSequence[2].Operation == WRITE_OPERATION, "Operation should be Write : %#v", mock)
|
||||||
|
assertBytesEqual(t, data[j:], mock.OutputCommandSequence[2].Data, "Write data should match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits the output into two parts and tests all such possible pairs
|
||||||
|
func helpsTestOutputSplitChunks(t *testing.T, data []byte) {
|
||||||
|
for i := 1; i < len(data)-1; i++ {
|
||||||
|
helpsTestOutputSplitChunksAtIndex(t, i, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splits the output in three parts and tests all such possible triples
|
||||||
|
func helpsTestOutputSplitThreeChunks(t *testing.T, data []byte) {
|
||||||
|
for i := 1; i < len(data)-2; i++ {
|
||||||
|
for j := i + 1; j < len(data)-1; j++ {
|
||||||
|
helpsTestOutputSplitThreeChunksAtIndex(t, data, i, j)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpsTestOutputSplitCommandsAtIndex(t *testing.T, data []byte, i int, plainText string, commands ...string) {
|
||||||
|
t.Logf("\ni=%d", i)
|
||||||
|
stdOut, _, _, mock := newBufferedMockTerm()
|
||||||
|
|
||||||
|
stdOut.Write(data[:i])
|
||||||
|
stdOut.Write(data[i:])
|
||||||
|
assertHandlerOutput(t, mock, plainText, commands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func helpsTestOutputSplitCommands(t *testing.T, data []byte, plainText string, commands ...string) {
|
||||||
|
for i := 1; i < len(data)-1; i++ {
|
||||||
|
helpsTestOutputSplitCommandsAtIndex(t, data, i, plainText, commands...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func injectCommandAt(data string, i int, command string) string {
|
||||||
|
retValue := make([]byte, len(data)+len(command)+4)
|
||||||
|
retValue = append(retValue, data[:i]...)
|
||||||
|
retValue = append(retValue, data[i:]...)
|
||||||
|
return string(retValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSplitChunks(t *testing.T) {
|
||||||
|
data := StringToBytes("qwertyuiopasdfghjklzxcvbnm")
|
||||||
|
helpsTestOutputSplitChunks(t, data)
|
||||||
|
helpsTestOutputSplitChunks(t, StringToBytes("BBBBB"))
|
||||||
|
helpsTestOutputSplitThreeChunks(t, StringToBytes("ABCDE"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOutputSplitChunksIncludingCommands(t *testing.T) {
|
||||||
|
helpsTestOutputSplitCommands(t, StringToBytes("Hello world.\x1B[mHello again."), "Hello world.Hello again.", "\x1B[m")
|
||||||
|
helpsTestOutputSplitCommandsAtIndex(t, StringToBytes("Hello world.\x1B[mHello again."), 2, "Hello world.Hello again.", "\x1B[m")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitChunkUnicode(t *testing.T) {
|
||||||
|
for _, l := range languages {
|
||||||
|
data := StringToBytes(l)
|
||||||
|
helpsTestOutputSplitChunks(t, data)
|
||||||
|
helpsTestOutputSplitThreeChunks(t, data)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue