diff --git a/terminal/plugins/org.eclipse.tm.terminal.control/src/org/eclipse/tm/internal/terminal/provisional/api/Logger.java b/terminal/plugins/org.eclipse.tm.terminal.control/src/org/eclipse/tm/internal/terminal/provisional/api/Logger.java index e993ff8b7b8..36096598ac4 100644 --- a/terminal/plugins/org.eclipse.tm.terminal.control/src/org/eclipse/tm/internal/terminal/provisional/api/Logger.java +++ b/terminal/plugins/org.eclipse.tm.terminal.control/src/org/eclipse/tm/internal/terminal/provisional/api/Logger.java @@ -55,6 +55,16 @@ public final class Logger { private static PrintStream logStream; private static StackWalker walker = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + private static boolean underTest = false; + + /** + * When underTest we want exception that are deep inside the code to be surfaced to the test + * @noreference This method is not intended to be referenced by clients. + */ + public static void setUnderTest(boolean underTest) { + Logger.underTest = underTest; + } + static { // Any of the known debugging options turns on the creation of the log file boolean createLogFile = TerminalPlugin.isOptionEnabled(TRACE_DEBUG_LOG) @@ -187,6 +197,9 @@ public final class Logger { * Writes an exception to the Terminal log and the Eclipse log */ public static final void logException(Exception ex) { + if (underTest) { + throw new RuntimeException("Terminal Under Test - examine cause for real failure", ex); //$NON-NLS-1$ + } logStatus(new Status(IStatus.ERROR, TerminalPlugin.PLUGIN_ID, IStatus.OK, ex.getMessage(), ex)); } diff --git a/terminal/plugins/org.eclipse.tm.terminal.test/META-INF/MANIFEST.MF b/terminal/plugins/org.eclipse.tm.terminal.test/META-INF/MANIFEST.MF index e92c74ac9bc..f7163566b19 100644 --- a/terminal/plugins/org.eclipse.tm.terminal.test/META-INF/MANIFEST.MF +++ b/terminal/plugins/org.eclipse.tm.terminal.test/META-INF/MANIFEST.MF @@ -8,7 +8,9 @@ Bundle-Localization: plugin Require-Bundle: org.junit, org.eclipse.tm.terminal.control;bundle-version="4.5.0", org.eclipse.core.runtime, - org.eclipse.ui + org.eclipse.ui, + org.junit.jupiter.api, + org.opentest4j Bundle-RequiredExecutionEnvironment: JavaSE-11 Export-Package: org.eclipse.tm.internal.terminal.connector;x-internal:=true, org.eclipse.tm.internal.terminal.emulator;x-internal:=true, diff --git a/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/MockTerminalControlForText.java b/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/MockTerminalControlForText.java new file mode 100644 index 00000000000..229691c575b --- /dev/null +++ b/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/MockTerminalControlForText.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2021 Kichwa Coders Canada Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.tm.internal.terminal.emulator; + +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.eclipse.tm.internal.terminal.control.impl.ITerminalControlForText; +import org.eclipse.tm.internal.terminal.provisional.api.ITerminalConnector; +import org.eclipse.tm.internal.terminal.provisional.api.TerminalState; + +public class MockTerminalControlForText implements ITerminalControlForText { + private List allTitles = new ArrayList<>(); + + @Override + public TerminalState getState() { + throw new UnsupportedOperationException(); + } + + @Override + public void setState(TerminalState state) { + throw new UnsupportedOperationException(); + } + + @Override + public void setTerminalTitle(String title) { + allTitles.add(title); + } + + public List getAllTitles() { + return Collections.unmodifiableList(allTitles); + } + + @Override + public ITerminalConnector getTerminalConnector() { + return null; + } + + @Override + public OutputStream getOutputStream() { + throw new UnsupportedOperationException(); + + } + + @Override + public void enableApplicationCursorKeys(boolean enable) { + throw new UnsupportedOperationException(); + + } + +} diff --git a/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/VT100EmulatorTest.java b/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/VT100EmulatorTest.java new file mode 100644 index 00000000000..e4c4e43f2bf --- /dev/null +++ b/terminal/plugins/org.eclipse.tm.terminal.test/src/org/eclipse/tm/internal/terminal/emulator/VT100EmulatorTest.java @@ -0,0 +1,215 @@ +/******************************************************************************* + * Copyright (c) 2021 Kichwa Coders Canada Inc. and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.tm.internal.terminal.emulator; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; +import java.util.StringJoiner; + +import org.eclipse.tm.internal.terminal.provisional.api.Logger; +import org.eclipse.tm.terminal.model.ITerminalTextData; +import org.eclipse.tm.terminal.model.TerminalTextDataFactory; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class VT100EmulatorTest { + + private static final int WINDOW_COLUMNS = 80; + private static final int WINDOW_LINES = 24; + private static final String CLEAR_CURSOR_TO_EOL = "\033[K"; + private static final String CURSOR_POSITION_TOP_LEFT = "\033[H"; + + /** + * Set the cursor position to line/column. Note that this is the logical + * line and column, so 1, 1 is the top left. + */ + private static String CURSOR_POSITION(int line, int column) { + return "\033[" + line + ";" + column + "H"; + } + + @BeforeAll + public static void beforeAll() { + Logger.setUnderTest(true); + } + + @AfterAll + public static void afterAll() { + Logger.setUnderTest(false); + } + + private ITerminalTextData data; + + private MockTerminalControlForText control = new MockTerminalControlForText(); + + private VT100Emulator emulator; + + @BeforeEach + public void before() { + data = TerminalTextDataFactory.makeTerminalTextData(); + emulator = new VT100Emulator(data, control, null); + emulator.resetState(); + emulator.setDimensions(WINDOW_LINES, WINDOW_COLUMNS); + } + + private Reader input(String... input) { + StringReader reader = new StringReader(String.join("", input)); + emulator.setInputStreamReader(reader); + return reader; + } + + private void run(String... input) { + Reader reader = input(input); + emulator.processText(); + try { + assertEquals(-1, reader.read()); + } catch (IOException e) { + throw new RuntimeException("Wrap exception so that run can be called in functions", e); + } + } + + /** + * Convert the data's char arrays into a string that can be compared with + * an expected array of lines. Each line in the data has its \0 characters + * changed to spaces and then stripTrailing is run. + * + * @param expectedArray lines that are joined with \n before testing against actual + */ + private void assertTextEquals(String... expectedArray) { + int height = data.getHeight(); + StringJoiner sj = new StringJoiner("\n"); + for (int i = 0; i < height; i++) { + char[] chars = data.getChars(i); + String line = chars == null ? "" : new String(chars); + String lineCleanedup = line.replace('\0', ' '); + String stripTrailing = lineCleanedup.stripTrailing(); + sj.add(stripTrailing); + } + String expected = String.join("\n", expectedArray).stripTrailing(); + String actual = sj.toString().stripTrailing(); + assertEquals(expected, actual); + } + + private void assertTextEquals(List expected) { + assertTextEquals(expected.toArray(String[]::new)); + } + + private void assertCursorLocation(int line, int column) { + assertAll(() -> assertEquals(line, data.getCursorLine(), "cursor line"), + () -> assertEquals(column, data.getCursorColumn(), "cursor column")); + } + + /** + * This tests the test harness ({@link #assertTextEquals(String...)} as much as the code. + */ + @Test + public void testBasicOperaiion() { + assertAll(() -> assertCursorLocation(0, 0), () -> assertTextEquals("")); + run("Hello"); + assertAll(() -> assertCursorLocation(0, 5), () -> assertTextEquals("Hello")); + emulator.clearTerminal(); + assertAll(() -> assertCursorLocation(0, 0), () -> assertTextEquals("")); + + // test multiline + emulator.clearTerminal(); + run("Hello 1\r\nHello 2"); + // test both ways of asserting multiple lines + assertAll(() -> assertCursorLocation(1, 7), // + () -> assertTextEquals("Hello 1\nHello 2"), // + () -> assertTextEquals("Hello 1", "Hello 2")); + + // test with no carriage return + emulator.clearTerminal(); + run("Hello 1\nHello 2"); + assertTextEquals("Hello 1", " Hello 2"); + + // test \b backspace + emulator.clearTerminal(); + run("Hello 1"); + assertAll(() -> assertCursorLocation(0, 7), () -> assertTextEquals("Hello 1")); + run("\b\b"); + assertAll(() -> assertCursorLocation(0, 5), () -> assertTextEquals("Hello 1")); + run(CLEAR_CURSOR_TO_EOL); + assertAll(() -> assertCursorLocation(0, 5), () -> assertTextEquals("Hello")); + } + + @Test + public void testMultiline() { + List expected = new ArrayList<>(); + for (int i = 0; i < data.getHeight(); i++) { + String line = "Hello " + i; + expected.add(line); + run(line); + if (i != data.getHeight() - 1) { + run("\r\n"); + } + } + assertTextEquals(expected); + + // add the final newline and check that the first line has been scrolled away + run("\r\n"); + expected.remove(0); + assertTextEquals(expected); + } + + @Test + public void testScrollBack() { + data.setMaxHeight(1000); + List expected = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + String line = "Hello " + i; + run(line + "\r\n"); + expected.add(line); + } + expected.remove(0); + assertTextEquals(expected); + } + + @Test + public void testCursorPosition() { + run(CURSOR_POSITION_TOP_LEFT); + assertAll(() -> assertCursorLocation(0, 0), () -> assertTextEquals("")); + run("Hello"); + assertAll(() -> assertCursorLocation(0, 5), () -> assertTextEquals("Hello")); + run(CURSOR_POSITION_TOP_LEFT); + assertAll(() -> assertCursorLocation(0, 0), () -> assertTextEquals("Hello")); + run(CURSOR_POSITION(2, 2)); + assertAll(() -> assertCursorLocation(1, 1), () -> assertTextEquals("Hello")); + emulator.clearTerminal(); + + data.setMaxHeight(1000); + List expected = new ArrayList<>(); + for (int i = 0; i < WINDOW_LINES; i++) { + String line = "Hello " + i; + run(line + "\r\n"); + expected.add(line); + } + assertAll(() -> assertCursorLocation(WINDOW_LINES, 0), () -> assertTextEquals(expected)); + run(CURSOR_POSITION_TOP_LEFT); + // because we added WINDOW_LINES number of lines, and ended it with a \r\n the first + // line we added is now in the scrollback, so the cursor is at line 1 + assertAll(() -> assertCursorLocation(1, 0), () -> assertTextEquals(expected)); + run("Bye \r\n"); + expected.set(1, "Bye"); + assertAll(() -> assertCursorLocation(2, 0), () -> assertTextEquals(expected)); + run(CURSOR_POSITION_TOP_LEFT); + assertAll(() -> assertCursorLocation(1, 0), () -> assertTextEquals(expected)); + run(CURSOR_POSITION(2, 2)); + assertAll(() -> assertCursorLocation(2, 1), () -> assertTextEquals(expected)); + } + +}