package terminal; import static com.googlecode.lanterna.input.KeyType.*; import static org.junit.Assert.*; import static terminal.LispTerminal.END_OF_SEGMENT; import java.io.*; import org.junit.*; import com.googlecode.lanterna.*; import com.googlecode.lanterna.input.*; import com.googlecode.lanterna.terminal.virtual.*; public class LispTerminalTest { private PipedInputStream inputReader; private PipedOutputStream inputWriter; private PipedInputStream outputReader; private PipedOutputStream outputWriter; private FlushListener flushListener; private VirtualTerminal virtualTerminal; private LispTerminal lispTerminal; private void pressKey(KeyType keyType) { virtualTerminal.addInput(new KeyStroke(keyType)); waitForFlushes(1); } private void pressKeyTimes(KeyType keyType, int times) { for (int i = times; i > 0; i--) virtualTerminal.addInput(new KeyStroke(keyType)); waitForFlushes(times); } private void pressControlKey(KeyType keyType) { virtualTerminal.addInput(new KeyStroke(keyType, true, false)); waitForFlushes(1); } private void enterCharacter(char character) { virtualTerminal.addInput(new KeyStroke(character, false, false)); waitForFlushes(1); } private void enterControlCharacter(char character) { virtualTerminal.addInput(new KeyStroke(character, true, false)); waitForFlushes(1); } private void enterCharacters(String characters) { for (char c : characters.toCharArray()) virtualTerminal.addInput(new KeyStroke(c, false, false)); waitForFlushes(characters.length()); } private void produceOutput(String output) { try { for (char c : output.toCharArray()) outputWriter.write(c); outputWriter.write(END_OF_SEGMENT); outputWriter.flush(); waitForFlushes(1); } catch (IOException ignored) {} } private void waitForFlushes(int flushCount) { try { synchronized (flushListener) { while (flushListener.getFlushCount() < flushCount) flushListener.wait(); flushListener.resetFlushCount(); } } catch (InterruptedException ignored) {} } private void setColumns(int columns) { int rows = virtualTerminal.getTerminalSize().getRows(); virtualTerminal.setTerminalSize(new TerminalSize(columns, rows)); } private void setRows(int rows) { int columns = virtualTerminal.getTerminalSize().getColumns(); virtualTerminal.setTerminalSize(new TerminalSize(columns, rows)); } private void assertCursorPosition(int column, int row) { assertEquals(column, virtualTerminal.getCursorPosition().getColumn()); assertEquals(row, virtualTerminal.getCursorPosition().getRow()); } private void assertCharacterAtPosition(char character, int column, int row) { TerminalPosition position = new TerminalPosition(column, row); String expected = String.valueOf(character); String actual = String.valueOf(virtualTerminal.getCharacter(position).getCharacter()); assertEquals(expected, actual); } private void assertCharacterPositions(char[][] positions) { for (int row = 0; row < positions.length; row++) for (int column = 0; column < positions[row].length; column++) assertCharacterAtPosition(positions[row][column], column, row); } private void assertInputWritten(String expected) { String actual = ""; try { inputWriter.close(); for (int c = inputReader.read(); c != -1; c = inputReader.read()) actual += (char) c; } catch (IOException ignored) {} assertEquals(expected, actual); } private void assertInputStreamClosed() { try { inputWriter.write(0); fail("input stream not closed"); } catch (IOException ignored) {} } @Before public void setUp() throws IOException { inputReader = new PipedInputStream(); inputWriter = new PipedOutputStream(inputReader); outputReader = new PipedInputStream(); outputWriter = new PipedOutputStream(outputReader); virtualTerminal = new DefaultVirtualTerminal(); flushListener = new FlushListener(); virtualTerminal.addVirtualTerminalListener(flushListener); lispTerminal = new LispTerminal(virtualTerminal, inputWriter, outputReader); lispTerminal.start(); } @After public void tearDown() throws IOException { lispTerminal.stop(); outputWriter.close(); } @Test public void leftArrowDoesNotMovePastOrigin() { pressKey(ArrowLeft); assertCursorPosition(0, 0); } @Test public void leftArrowWorksAfterEnteringCharacters() { enterCharacters("abc"); assertCursorPosition(3, 0); pressKey(ArrowLeft); assertCursorPosition(2, 0); pressKey(ArrowLeft); assertCursorPosition(1, 0); pressKey(ArrowLeft); assertCursorPosition(0, 0); pressKey(ArrowLeft); assertCursorPosition(0, 0); } @Test public void leftArrowWorksAcrossRows() { setColumns(5); enterCharacters("123451"); assertCursorPosition(1, 1); pressKeyTimes(ArrowLeft, 2); assertCursorPosition(4, 0); } @Test public void rightArrowDoesNotMovePastEndOfInput() { pressKey(ArrowRight); assertCursorPosition(0, 0); } @Test public void rightArrowWorksAfterMovingLeft() { enterCharacters("12"); assertCursorPosition(2, 0); pressKey(ArrowLeft); assertCursorPosition(1, 0); pressKey(ArrowRight); assertCursorPosition(2, 0); pressKey(ArrowRight); assertCursorPosition(2, 0); } @Test public void rightArrowWorksAcrossRow() { setColumns(5); enterCharacters("123451"); assertCursorPosition(1, 1); pressKeyTimes(ArrowLeft, 3); assertCursorPosition(3, 0); pressKeyTimes(ArrowRight, 3); assertCursorPosition(1, 1); } @Test public void characterKeyIsEchoed() { enterCharacter('a'); assertCursorPosition(1, 0); assertCharacterAtPosition('a', 0, 0); } @Test public void characterIsInserted() { enterCharacters("abcd"); pressKeyTimes(ArrowLeft, 2); enterCharacter('x'); assertCharacterPositions(new char[][] { { 'a', 'b', 'x', 'c', 'd' } }); } @Test public void characterIsInserted_PushesInputToNextRow() { setColumns(4); enterCharacters("abcd"); pressKeyTimes(ArrowLeft, 2); enterCharacter('x'); assertCharacterPositions(new char[][] { { 'a', 'b', 'x', 'c' }, { 'd', ' ', ' ', ' ' } }); } @Test public void backspaceDoesNothingAtOrigin() { pressKey(Backspace); assertCursorPosition(0, 0); } @Test public void backspaceWorksAfterInput() { enterCharacters("12345"); pressKeyTimes(Backspace, 2); assertCursorPosition(3, 0); assertCharacterPositions(new char[][] { { '1', '2', '3', ' ', ' ', ' ' } }); } @Test public void backspaceWorksAcrossRow() { setColumns(4); enterCharacters("1234567"); pressKeyTimes(Backspace, 5); assertCursorPosition(2, 0); assertCharacterPositions(new char[][] { { '1', '2', ' ', ' ' }, { ' ', ' ', ' ', ' ' } }); } @Test public void backspaceWorksInMiddleOfInput() { enterCharacters("12345"); pressKeyTimes(ArrowLeft, 2); pressKey(Backspace); assertCursorPosition(2, 0); assertCharacterPositions(new char[][] { { '1', '2', '4', '5' } }); } @Test public void deleteDoesNothingAtOrigin() { pressKey(Delete); assertCursorPosition(0, 0); } @Test public void deleteDoesNothingAtEndOfInput() { enterCharacters("del"); pressKey(Delete); assertCursorPosition(3, 0); assertCharacterPositions(new char[][] { { 'd', 'e', 'l' } }); } @Test public void deleteWorksAtStartOfInput() { enterCharacters("del"); pressKeyTimes(ArrowLeft, 3); pressKeyTimes(Delete, 3); assertCursorPosition(0, 0); assertCharacterPositions(new char[][] { { ' ', ' ', ' ' } }); } @Test public void deleteWorksAcrossRow() { setColumns(4); enterCharacters("delete"); pressKeyTimes(ArrowLeft, 5); pressKey(Delete); assertCursorPosition(1, 0); assertCharacterPositions(new char[][] { { 'd', 'l', 'e', 't' }, { 'e', ' ' } }); } @Test public void enterMovesToNextLine() { pressKey(Enter); assertCursorPosition(0, 1); } @Test public void enterWritesLineToPipedStream() { enterCharacters("enter"); pressKey(Enter); assertInputWritten("enter\n"); } @Test public void enterPressedInMiddleOfInput_WritesEntireLineToPipedStream() { enterCharacters("enter"); pressKeyTimes(ArrowLeft, 2); pressKey(Enter); assertInputWritten("enter\n"); } @Test public void enterAfterInsertedText_WritesLineToPipedStream() { enterCharacters("enter"); pressKeyTimes(ArrowLeft, 2); enterCharacters("||"); pressKey(Enter); assertInputWritten("ent||er\n"); } @Test public void enterAfterBackspace_WritesLineToPipedStream() { enterCharacters("enter"); pressKeyTimes(Backspace, 2); pressKey(Enter); assertInputWritten("ent\n"); } @Test public void enterAfterDelete_WritesLineToPipedStream() { enterCharacters("enter"); pressKeyTimes(ArrowLeft, 2); pressKeyTimes(Delete, 2); pressKey(Enter); assertInputWritten("ent\n"); } @Test public void controlDWorks() { enterCharacters("control-d"); enterControlCharacter('d'); assertInputStreamClosed(); assertInputWritten("control-d\n"); } @Test public void controlDWorksInMiddleOfInput() { enterCharacters("control-d"); pressKeyTimes(ArrowLeft, 2); enterControlCharacter('d'); assertInputStreamClosed(); assertInputWritten("control-d\n"); } @Test public void escapeDoesNothing() { pressKey(Escape); assertCursorPosition(0, 0); assertInputWritten(""); } @Test public void controlQDoesNothing() { enterControlCharacter('q'); assertCursorPosition(0, 0); assertInputWritten(""); } @Test public void controlEnterDoesNothing() { pressControlKey(Enter); assertCursorPosition(0, 0); assertInputWritten(""); } @Test public void outputIsWritten() { produceOutput("output"); assertCursorPosition(6, 0); assertCharacterPositions(new char[][] { { 'o', 'u', 't', 'p', 'u', 't' } }); } @Test public void endOfSegmentCharacterIsNotPrinted() { produceOutput("> " + END_OF_SEGMENT); assertCursorPosition(2, 0); assertCharacterPositions(new char[][] { { '>', ' ', ' ' } }); } @Test public void enterTextPastLastLineOfBuffer() { setColumns(3); setRows(2); enterCharacters("01201201"); assertCursorPosition(2, 1); assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', ' ' } }); } @Test public void insertingTextPushesInputPastEndOfBuffer() { setColumns(3); setRows(4); pressKey(Enter); enterCharacters("00011122"); pressKeyTimes(ArrowLeft, 4); assertCursorPosition(1, 2); enterCharacters("zz"); assertCursorPosition(0, 2); assertCharacterPositions(new char[][] { { '0', '0', '0' }, { '1', 'z', 'z' }, { '1', '1', '2' }, { '2', ' ', ' ' } }); } @Test public void insertingTextDoesNothingWhenBufferFilled() { setColumns(3); setRows(3); enterCharacters("00011122"); pressKeyTimes(ArrowLeft, 4); assertCursorPosition(1, 1); enterCharacters("zz"); assertCursorPosition(1, 1); assertCharacterPositions(new char[][] { { '0', '0', '0' }, { '1', '1', '1' }, { '2', '2', ' ' } }); } @Test public void appendingTextDoesNothingWhenBufferFilled() { setColumns(3); setRows(3); enterCharacters("000111222333444"); assertCursorPosition(2, 2); assertCharacterPositions(new char[][] { { '0', '0', '0' }, { '1', '1', '1' }, { '2', '2', ' ' } }); } @Test public void printedOutputToEndOfRow_MovesCursorToNextRow() { setColumns(3); produceOutput("out"); assertCursorPosition(0, 1); } @Test public void printedOutputToEndOfBuffer_MovesCursorToNewRow() { setColumns(3); setRows(2); produceOutput("output"); assertCursorPosition(0, 1); assertCharacterPositions(new char[][] { { 'p', 'u', 't' }, { ' ', ' ', ' ' } }); } @Test public void printedOutputDoesNotOverwriteInput() { setColumns(3); enterCharacters("01201201"); pressKeyTimes(ArrowLeft, 5); produceOutput("out"); assertCursorPosition(0, 4); assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', '2' }, { '0', '1', 'o' }, { 'u', 't', ' ' } }); } @Test public void printedOutputDoesNotOverwriteInput_AfterEnter() { setColumns(3); enterCharacters("01201201"); pressKeyTimes(ArrowLeft, 5); pressKey(Enter); produceOutput("out"); assertCursorPosition(0, 4); assertCharacterPositions(new char[][] { { '0', '1', '2' }, { '0', '1', '2' }, { '0', '1', ' ' }, { 'o', 'u', 't' } }); } @Test public void resizeIsHandledGracefully() { enterCharacters("resize"); pressKey(Enter); enterCharacters("test"); setColumns(3); assertCursorPosition(1, 1); assertCharacterPositions(new char[][] { { 't', 'e', 's' }, { 't', ' ', ' ' } }); } @Test public void backspaceWorksAfterResize() { enterCharacters("resize"); pressKey(Enter); enterCharacters("test"); setColumns(3); pressKeyTimes(Backspace, 20); assertCursorPosition(0, 0); assertCharacterPositions(new char[][] { { ' ', ' ', ' ' }, { ' ', ' ', ' ' } }); } @Test public void deleteWorksAfterResize() { enterCharacters("resize"); pressKey(Enter); enterCharacters("test"); setColumns(3); pressKeyTimes(ArrowLeft, 20); pressKeyTimes(Delete, 20); pressKeyTimes(ArrowRight, 20); assertCursorPosition(0, 0); assertCharacterPositions(new char[][] { { ' ', ' ', ' ' }, { ' ', ' ', ' ' } }); } @Test public void controlSequenceIsNotPrinted() { produceOutput("\u001B[32mcontrol\u001B[0mseq"); assertCharacterPositions(new char[][] { { 'c', 'o', 'n', 't', 'r', 'o', 'l', 's', 'e', 'q' } }); } }