Clean up terminal code and unit tests

The terminal unit tests were updated so that they don't rely on an
arbitrary delay.
This commit is contained in:
Mike Cifelli 2017-03-19 12:54:35 -04:00
parent 072a432026
commit 38ab1144fb
5 changed files with 413 additions and 139 deletions

View File

@ -1,8 +1,12 @@
package main;
import static terminal.LispTerminal.END_OF_SEGMENT;
import java.io.*;
import java.util.function.Function;
import com.googlecode.lanterna.terminal.*;
import interpreter.*;
import terminal.LispTerminal;
@ -17,25 +21,27 @@ public class LispMain {
private PipedOutputStream inputWriter;
private PipedInputStream outputReader;
private PipedOutputStream outputWriter;
private LispTerminal terminal;
private LispTerminal lispTerminal;
private LispMain() throws IOException {
inputReader = new PipedInputStream();
inputWriter = new PipedOutputStream(inputReader);
outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
terminal = new LispTerminal(inputWriter, outputReader);
lispTerminal = new LispTerminal(createIOSafeTerminal(), inputWriter, outputReader);
}
private IOSafeTerminal createIOSafeTerminal() throws IOException {
return IOSafeTerminalAdapter.createRuntimeExceptionConvertingAdapter(new DefaultTerminalFactory().createTerminal());
}
public static void main(String[] args) throws IOException {
LispMain main = new LispMain();
main.run(args);
new LispMain().run(args);
}
private void run(String[] args) {
if (args.length == 0)
terminal.run();
lispTerminal.run();
LispInterpreter interpreter = buildInterpreter(args);
interpreter.interpret();
@ -79,7 +85,7 @@ public class LispMain {
private void shutdown() {
try {
terminal.finish();
lispTerminal.finish();
outputWriter.close();
} catch (IOException e) {
// TODO Auto-generated catch block
@ -120,7 +126,7 @@ public class LispMain {
@Override
public String apply(String s) {
return s + 'x';
return s + END_OF_SEGMENT;
}
};
}

View File

@ -0,0 +1,26 @@
package terminal;
import static terminal.ControlSequenceHandler.Command.SGR;
class ControlSequenceHandler {
public static final boolean isEscape(char c) {
return c == '\u001B';
}
private boolean inControlSequence;
private int code;
private Command command;
public ControlSequenceHandler() {
// TODO Auto-generated constructor stub
this.inControlSequence = false;
this.code = 0;
this.command = SGR;
}
public static enum Command {
SGR
}
}

View File

@ -1,95 +1,55 @@
package terminal;
import static terminal.ControlSequenceHandler.isEscape;
import static util.Characters.EOF;
import java.io.*;
import java.util.concurrent.*;
import com.googlecode.lanterna.TerminalPosition;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.terminal.*;
import com.googlecode.lanterna.terminal.IOSafeTerminal;
public class LispTerminal {
// private static final String ANSI_RESET = "001B[0m";
// private static final String ANSI_RED = "\u001B[31m";
// private static final String ANSI_GREEN = "\u001B[32m";
// private static final String ANSI_YELLOW = "\u001B[33m";
// private static final String ANSI_PURPLE = "\u001B[35m";
public static final char END_OF_SEGMENT = 'x';
private IOSafeTerminal terminal;
private boolean isFinished;
private String currentLine;
private TerminalPosition origin;
private PipedOutputStream inputWriter;
private PipedInputStream outputReader;
private ExecutorService executor;
private ExecutorService executorService;
private ControlSequenceHandler controlSequenceHandler;
private TerminalPosition origin;
private String inputLine;
private String outputSegment;
private boolean isFinished;
LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) {
public LispTerminal(IOSafeTerminal terminal, PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.terminal = terminal;
initialize(inputWriter, outputReader);
}
public LispTerminal(PipedOutputStream inputWriter, PipedInputStream outputReader) {
try {
Terminal unsafe = new DefaultTerminalFactory().createTerminal();
this.terminal = IOSafeTerminalAdapter.createRuntimeExceptionConvertingAdapter(unsafe);
} catch (IOException e) {
// TODO
e.printStackTrace();
}
initialize(inputWriter, outputReader);
}
private void initialize(PipedOutputStream inputWriter, PipedInputStream outputReader) {
this.isFinished = false;
this.currentLine = "";
this.origin = terminal.getCursorPosition();
this.inputWriter = inputWriter;
this.outputReader = outputReader;
this.executor = Executors.newFixedThreadPool(2);
this.executorService = Executors.newFixedThreadPool(2);
this.controlSequenceHandler = new ControlSequenceHandler();
this.origin = terminal.getCursorPosition();
this.inputLine = "";
this.outputSegment = "";
this.isFinished = false;
}
public void run() {
executor.execute(this::readInput);
executor.execute(this::writeOutput);
executor.shutdown();
executorService.execute(this::readInput);
executorService.execute(this::writeOutput);
executorService.shutdown();
}
public void readInput() {
while (!isFinished) {
handleKeyStroke(getKeyStroke());
terminal.flush();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
isFinished = true;
}
}
while (!isFinished)
processNextKey();
}
public void writeOutput() {
try {
for (int c = outputReader.read(); c != -1; c = outputReader.read()) {
synchronized (this) {
terminal.setCursorVisible(false);
if (c == 'x') {
terminal.flush();
origin = terminal.getCursorPosition();
terminal.setCursorVisible(true);
} else {
terminal.putCharacter((char) c);
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
terminal.setCursorVisible(true);
terminal.close();
private void processNextKey() {
handleKey(getKeyStroke());
takeNap();
}
private KeyStroke getKeyStroke() {
@ -98,6 +58,7 @@ public class LispTerminal {
try {
keyStroke = terminal.pollInput();
} catch (IllegalStateException e) {
// TODO - Issue #299
terminal.putCharacter('\n');
System.exit(0);
}
@ -105,14 +66,19 @@ public class LispTerminal {
return keyStroke;
}
private synchronized void handleKeyStroke(KeyStroke keyStroke) {
private synchronized void handleKey(KeyStroke keyStroke) {
if (keyStroke == null)
return;
doKey(keyStroke);
terminal.flush();
}
private synchronized void doKey(KeyStroke keyStroke) {
KeyType keyType = keyStroke.getKeyType();
if (keyStroke.isCtrlDown())
doControlKeyCharacter(keyStroke);
doControlKey(keyStroke);
else if (keyType == KeyType.ArrowLeft)
moveCursorLeft();
else if (keyType == KeyType.ArrowRight)
@ -127,40 +93,28 @@ public class LispTerminal {
doCharacter(keyStroke);
}
private synchronized void doControlKeyCharacter(KeyStroke keyStroke) {
private synchronized void doControlKey(KeyStroke keyStroke) {
KeyType keyType = keyStroke.getKeyType();
if (keyType == KeyType.Character)
if (keyStroke.getCharacter() == 'd') {
doControlCharacter(keyStroke);
}
private synchronized void doControlCharacter(KeyStroke keyStroke) {
if (keyStroke.getCharacter() == 'd')
doControlD();
}
private synchronized void doControlD() {
doEnter();
finish();
}
}
private synchronized void doEnter() {
terminal.putCharacter('\n');
currentLine += "\n";
try {
inputWriter.write(currentLine.getBytes());
inputWriter.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
currentLine = "";
origin = terminal.getCursorPosition();
}
private synchronized void moveCursorLeft() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition))
if (cursorPosition.getColumn() == 0)
terminal.setCursorPosition(terminal.getTerminalSize().getColumns(), cursorPosition.getRow() - 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
retractCursor(cursorPosition);
}
private synchronized boolean isPossibleToMoveLeft(TerminalPosition cursorPosition) {
@ -168,13 +122,22 @@ public class LispTerminal {
}
private synchronized int distanceFromOrigin(TerminalPosition cursorPosition) {
int cursorColumn = cursorPosition.getColumn();
int cursorRow = cursorPosition.getRow();
int originColumn = origin.getColumn();
int originRow = origin.getRow();
int columnDifference = cursorPosition.getColumn() - origin.getColumn();
int rowDifference = cursorPosition.getRow() - origin.getRow();
int totalColumns = terminal.getTerminalSize().getColumns();
return cursorColumn - originColumn + (totalColumns * (cursorRow - originRow));
return columnDifference + (totalColumns * rowDifference);
}
private synchronized void retractCursor(TerminalPosition cursorPosition) {
if (isAtStartOfRow(cursorPosition))
terminal.setCursorPosition(terminal.getTerminalSize().getColumns(), cursorPosition.getRow() - 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
}
private boolean isAtStartOfRow(TerminalPosition cursorPosition) {
return cursorPosition.getColumn() == 0;
}
private synchronized void moveCursorRight() {
@ -184,42 +147,52 @@ public class LispTerminal {
advanceCursor(cursorPosition);
}
private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) < inputLine.length();
}
private synchronized void advanceCursor(TerminalPosition cursorPosition) {
if (isCursorAtEndOfRow(cursorPosition))
if (isAtEndOfRow(cursorPosition))
terminal.setCursorPosition(0, cursorPosition.getRow() + 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(1));
}
private synchronized boolean isCursorAtEndOfRow(TerminalPosition cursorPosition) {
private synchronized boolean isAtEndOfRow(TerminalPosition cursorPosition) {
return cursorPosition.getColumn() == terminal.getTerminalSize().getColumns() - 1;
}
private synchronized boolean isPossibleToMoveRight(TerminalPosition cursorPosition) {
return distanceFromOrigin(cursorPosition) < currentLine.length();
private synchronized void doEnter() {
terminal.putCharacter('\n');
inputLine += "\n";
try {
inputWriter.write(inputLine.getBytes());
inputWriter.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
inputLine = "";
origin = terminal.getCursorPosition();
}
private synchronized void doBackspace() {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveLeft(cursorPosition)) {
String remaining = currentLine.substring(distanceFromOrigin(cursorPosition), currentLine.length());
currentLine = currentLine.substring(0, distanceFromOrigin(cursorPosition) - 1) + remaining;
String remaining = inputLine.substring(distanceFromOrigin(cursorPosition), inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition) - 1) + remaining;
if (cursorPosition.getColumn() == 0)
terminal.setCursorPosition(terminal.getTerminalSize().getColumns(), cursorPosition.getRow() - 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
retractCursor(cursorPosition);
for (char c : remaining.toCharArray())
terminal.putCharacter(c);
terminal.putCharacter(' ');
if (cursorPosition.getColumn() == 0)
terminal.setCursorPosition(terminal.getTerminalSize().getColumns(), cursorPosition.getRow() - 1);
else
terminal.setCursorPosition(cursorPosition.withRelativeColumn(-1));
retractCursor(cursorPosition);
}
}
@ -227,8 +200,8 @@ public class LispTerminal {
TerminalPosition cursorPosition = terminal.getCursorPosition();
if (isPossibleToMoveRight(cursorPosition)) {
String remaining = currentLine.substring(distanceFromOrigin(cursorPosition) + 1, currentLine.length());
currentLine = currentLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining;
String remaining = inputLine.substring(distanceFromOrigin(cursorPosition) + 1, inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining;
for (char c : remaining.toCharArray())
terminal.putCharacter(c);
@ -243,8 +216,8 @@ public class LispTerminal {
if (isPossibleToMoveRight(cursorPosition)) {
String remaining = keyStroke.getCharacter()
+ currentLine.substring(distanceFromOrigin(cursorPosition), currentLine.length());
currentLine = currentLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining;
+ inputLine.substring(distanceFromOrigin(cursorPosition), inputLine.length());
inputLine = inputLine.substring(0, distanceFromOrigin(cursorPosition)) + remaining;
for (char c : remaining.toCharArray())
terminal.putCharacter(c);
@ -252,12 +225,61 @@ public class LispTerminal {
advanceCursor(cursorPosition);
} else {
terminal.putCharacter(keyStroke.getCharacter());
currentLine += keyStroke.getCharacter();
inputLine += keyStroke.getCharacter();
advanceCursor(cursorPosition);
}
}
private void takeNap() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
isFinished = true;
}
}
public void writeOutput() {
try {
for (int c = outputReader.read(); c != EOF; c = outputReader.read())
processOutput((char) c);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
terminal.setCursorVisible(true);
terminal.flush();
terminal.close();
}
private synchronized void processOutput(char c) {
if (isEscape(c))
parseControlSequence();
else if (isEndOfSegment(c))
printSegment();
else
outputSegment += c;
}
private synchronized void parseControlSequence() {}
private synchronized boolean isEndOfSegment(char c) {
return c == END_OF_SEGMENT;
}
private synchronized void printSegment() {
terminal.setCursorVisible(false);
for (char c : outputSegment.toCharArray())
terminal.putCharacter(c);
terminal.flush();
terminal.setCursorVisible(true);
outputSegment = "";
origin = terminal.getCursorPosition();
}
public void finish() {
isFinished = true;

View File

@ -0,0 +1,38 @@
package terminal;
import com.googlecode.lanterna.TerminalSize;
import com.googlecode.lanterna.terminal.Terminal;
import com.googlecode.lanterna.terminal.virtual.VirtualTerminalListener;
public class FlushListener implements VirtualTerminalListener {
private int flushCount;
public FlushListener() {
this.flushCount = 0;
}
public int getFlushCount() {
return flushCount;
}
public void resetFlushCount() {
flushCount = 0;
}
@Override
public void onResized(Terminal terminal, TerminalSize newSize) {}
@Override
public synchronized void onFlush() {
flushCount++;
notify();
}
@Override
public void onBell() {}
@Override
public void onClose() {}
}

View File

@ -1,6 +1,7 @@
package terminal;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
import static terminal.LispTerminal.END_OF_SEGMENT;
import java.io.*;
@ -10,52 +11,100 @@ import com.googlecode.lanterna.*;
import com.googlecode.lanterna.input.*;
import com.googlecode.lanterna.terminal.virtual.*;
public class LispTerminalAttempt {
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));
sleep();
waitForFlushes(1);
}
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));
sleep();
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));
sleep();
waitForFlushes(characters.length());
}
private void sleep() {
private void produceOutput(String output) {
try {
Thread.sleep(15);
for (char c : output.toCharArray())
outputWriter.write(c);
outputWriter.write(END_OF_SEGMENT);
outputWriter.flush();
waitForFlushes(1);
} catch (IOException e) {}
}
private void waitForFlushes(int flushCount) {
try {
synchronized (flushListener) {
while (flushListener.getFlushCount() < flushCount)
flushListener.wait();
flushListener.resetFlushCount();
}
} catch (InterruptedException e) {}
}
private void assertCursorPosition(int column, int row) {
assertTrue(virtualTerminal.getCursorBufferPosition().getColumn() == column);
assertTrue(virtualTerminal.getCursorBufferPosition().getRow() == row);
}
private void assertCharacterAtPosition(char character, int column, int row) {
TerminalPosition position = new TerminalPosition(column, row);
assertTrue(virtualTerminal.getBufferCharacter(position).getCharacter() == character);
}
private void setColumns(int columns) {
virtualTerminal.setTerminalSize(new TerminalSize(columns, virtualTerminal.getTerminalSize().getRows()));
}
private void assertCursorPosition(int column, int row) {
assertEquals(column, virtualTerminal.getCursorBufferPosition().getColumn());
assertEquals(row, virtualTerminal.getCursorBufferPosition().getRow());
}
private void assertCharacterAtPosition(char character, int column, int row) {
TerminalPosition position = new TerminalPosition(column, row);
assertEquals(character, virtualTerminal.getBufferCharacter(position).getCharacter());
}
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 e) {}
assertEquals(expected, actual);
}
private void assertInputStreamClosed() {
try {
inputWriter.write(0);
fail("input stream not closed");
} catch (IOException e) {}
}
@Before
public void setUp() throws IOException {
inputReader = new PipedInputStream();
@ -63,6 +112,8 @@ public class LispTerminalAttempt {
outputReader = new PipedInputStream();
outputWriter = new PipedOutputStream(outputReader);
virtualTerminal = new DefaultVirtualTerminal();
flushListener = new FlushListener();
virtualTerminal.addVirtualTerminalListener(flushListener);
lispTerminal = new LispTerminal(virtualTerminal, inputWriter, outputReader);
lispTerminal.run();
}
@ -222,4 +273,135 @@ public class LispTerminalAttempt {
assertCharacterAtPosition('5', 3, 0);
}
@Test
public void deleteDoesNothingAtOrigin() {
pressKey(KeyType.Delete);
assertCursorPosition(0, 0);
}
@Test
public void deleteDoesNothingAtEndOfInput() {
enterCharacters("del");
pressKey(KeyType.Delete);
assertCursorPosition(3, 0);
assertCharacterAtPosition('d', 0, 0);
assertCharacterAtPosition('e', 1, 0);
assertCharacterAtPosition('l', 2, 0);
}
@Test
public void deleteWorksAtStartOfInput() {
enterCharacters("del");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.Delete);
pressKey(KeyType.Delete);
pressKey(KeyType.Delete);
assertCursorPosition(0, 0);
assertCharacterAtPosition(' ', 0, 0);
assertCharacterAtPosition(' ', 1, 0);
assertCharacterAtPosition(' ', 2, 0);
}
@Test
public void deleteWorksAcrossRow() {
setColumns(4);
enterCharacters("delete");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.Delete);
assertCursorPosition(1, 0);
assertCharacterAtPosition('d', 0, 0);
assertCharacterAtPosition('l', 1, 0);
assertCharacterAtPosition('e', 2, 0);
assertCharacterAtPosition('t', 3, 0);
assertCharacterAtPosition('e', 0, 1);
assertCharacterAtPosition(' ', 1, 1);
}
@Test
public void enterMovesToNextLine() {
pressKey(KeyType.Enter);
assertCursorPosition(0, 1);
}
@Test
public void enterWritesLineToPipedStream() {
enterCharacters("enter");
pressKey(KeyType.Enter);
assertInputWritten("enter\n");
}
@Test
public void enterPressedInMiddleOfInput_WritesEntireLineToPipedStream() {
enterCharacters("enter");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.Enter);
assertInputWritten("enter\n");
}
@Test
public void controlDWorks() {
enterCharacters("control-d");
enterControlCharacter('d');
assertInputStreamClosed();
assertInputWritten("control-d\n");
}
@Test
public void controlDWorksInMiddleOfInput() {
enterCharacters("control-d");
pressKey(KeyType.ArrowLeft);
pressKey(KeyType.ArrowLeft);
enterControlCharacter('d');
assertInputStreamClosed();
assertInputWritten("control-d\n");
}
@Test
public void escapeDoesNothing() {
pressKey(KeyType.Escape);
assertCursorPosition(0, 0);
assertInputWritten("");
}
@Test
public void controlQDoesNothing() {
enterControlCharacter('q');
assertCursorPosition(0, 0);
assertInputWritten("");
}
@Test
public void controlEnterDoesNothing() {
pressControlKey(KeyType.Enter);
assertCursorPosition(0, 0);
assertInputWritten("");
}
@Test
public void outputIsWritten() {
produceOutput("output");
assertCharacterAtPosition('o', 0, 0);
assertCharacterAtPosition('u', 1, 0);
assertCharacterAtPosition('t', 2, 0);
assertCharacterAtPosition('p', 3, 0);
assertCharacterAtPosition('u', 4, 0);
assertCharacterAtPosition('t', 5, 0);
}
@Test
public void endOfSegmentCharacterIsNotPrinted() {
produceOutput("> " + END_OF_SEGMENT);
assertCursorPosition(2, 0);
assertCharacterAtPosition('>', 0, 0);
assertCharacterAtPosition(' ', 1, 0);
assertCharacterAtPosition(' ', 2, 0);
}
}