moby--moby/pkg/term/winconsole/term_emulator_test.go

389 lines
14 KiB
Go

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 received
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)
}
}