diff options
author | Paul Pazderski | 2019-01-20 11:50:32 +0000 |
---|---|---|
committer | Paul Pazderski | 2019-05-07 22:28:19 +0000 |
commit | 194c7e64b3eb37217a6b880827c4def091ffd888 (patch) | |
tree | 382789238d46848542b556d1e6aea6820b5299b8 | |
parent | 6407443235147c082852c2d93d9fa5b749d9ea39 (diff) | |
download | eclipse.platform.debug-194c7e64b3eb37217a6b880827c4def091ffd888.tar.gz eclipse.platform.debug-194c7e64b3eb37217a6b880827c4def091ffd888.tar.xz eclipse.platform.debug-194c7e64b3eb37217a6b880827c4def091ffd888.zip |
Bug 547063 - [console] Tests for IOConsoleI20190508-0450I20190508-0150
Especially tests for IOConsolePartitioner and IOConsoleViewer.
Change-Id: I7d2f9a76cea6529b344a51e22aa3f623659fb900
Signed-off-by: Paul Pazderski <paul-eclipse@ppazderski.de>
3 files changed, 999 insertions, 0 deletions
diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java index 4b1ddcee8..36beec028 100644 --- a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java +++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/AutomatedSuite.java @@ -19,6 +19,7 @@ import org.eclipse.debug.tests.breakpoint.BreakpointOrderingTests; import org.eclipse.debug.tests.console.ConsoleDocumentAdapterTests; import org.eclipse.debug.tests.console.ConsoleManagerTests; import org.eclipse.debug.tests.console.ConsoleTests; +import org.eclipse.debug.tests.console.IOConsoleTests; import org.eclipse.debug.tests.console.ProcessConsoleManagerTests; import org.eclipse.debug.tests.console.ProcessConsoleTests; import org.eclipse.debug.tests.launching.AcceleratorSubstitutionTests; @@ -113,6 +114,7 @@ public class AutomatedSuite extends TestSuite { addTest(new TestSuite(ConsoleDocumentAdapterTests.class)); addTest(new TestSuite(ConsoleManagerTests.class)); addTest(new TestSuite(ConsoleTests.class)); + addTest(new TestSuite(IOConsoleTests.class)); addTest(new TestSuite(ProcessConsoleTests.class)); addTest(new TestSuite(ProcessConsoleManagerTests.class)); diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java new file mode 100644 index 000000000..2d563e511 --- /dev/null +++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTestUtil.java @@ -0,0 +1,665 @@ +/******************************************************************************* + * Copyright (c) 2019 Paul Pazderski and others. + * + * 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 + * + * Contributors: + * Paul Pazderski - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.debug.tests.TestUtil; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentPartitioner; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ST; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Event; +import org.eclipse.ui.console.IConsoleDocumentPartitioner; +import org.eclipse.ui.console.IOConsole; +import org.eclipse.ui.console.IOConsoleOutputStream; + +import junit.framework.TestCase; + +/** + * Utility to help testing input and output in {@link IOConsole}. + */ +public final class IOConsoleTestUtil { + /** The tested console. */ + private final IOConsole console; + /** Document of the tested console. */ + private final IDocument doc; + /** Text widget of the tested console. */ + private final StyledText textPanel; + /** The tested consoles document partitioner. */ + private final IConsoleDocumentPartitioner partitioner; + /** Valid partition types of the used document partitioner. */ + private final List<String> validPartionTypes; + /** Name used for some logging purpose. */ + private final String name; + /** + * The default output stream used to simulate output in console. Lazy + * initialized. + */ + private IOConsoleOutputStream defaultOut = null; + + /** + * Create a new testing helper. + * + * @param console the console to test + * @param textPanel the consoles text widget. May be <code>null</code> but + * none of the user input or caret movement methods can be used + * in this case. + * @param name name of the caller which will be logged as prefix if some job + * waiting timed out. + */ + public IOConsoleTestUtil(IOConsole console, StyledText textPanel, String name) { + this.console = console; + TestCase.assertNotNull(this.console); + this.doc = console.getDocument(); + TestCase.assertNotNull(this.doc); + final Class<?> expectedInterface = IConsoleDocumentPartitioner.class; + TestCase.assertTrue("Expected partitioner implements " + expectedInterface.getName() + // + ". Found: " + this.doc.getDocumentPartitioner().getClass(), // + expectedInterface.isAssignableFrom(this.doc.getDocumentPartitioner().getClass())); + this.partitioner = (IConsoleDocumentPartitioner) this.doc.getDocumentPartitioner(); + this.validPartionTypes = Arrays.asList(this.partitioner.getLegalContentTypes()); + this.textPanel = textPanel; + this.name = name; + } + + /** + * Clear console. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil clear() throws Exception { + console.clearConsole(); + waitForScheduledJobs(); + TestCase.assertEquals("Console is not cleared.", 0, doc.getLength()); + return this; + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream but do not wait until input was actually written to console. + * + * @param s content to write in output stream + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #write(String) + */ + public IOConsoleTestUtil writeFast(final String s) throws IOException { + return writeFast(s, getDefaultOutputStream()); + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream and wait until content actually arrived in console. + * + * @param s content to write in output stream + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #writeFast(String) + */ + public IOConsoleTestUtil write(final String s) throws Exception { + return write(s, getDefaultOutputStream()); + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream and verify new console content and partitioning. Expects written + * string was appended to end of previous console content and has not + * overwritten or removed any previous content. Further expects that + * partitioning of new written content is not mixed with other partition + * types. + * + * @param s content to write in output stream + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil writeAndVerify(final String s) throws Exception { + return writeAndVerify(s, getDefaultOutputStream()); + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream but do not wait until input was actually written to console. + * + * @param s content to write in output stream + * @param out use this output stream instead of default one + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #write(String, IOConsoleOutputStream) + */ + public IOConsoleTestUtil writeFast(final String s, IOConsoleOutputStream out) throws IOException { + out.write(s); + return this; + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream and wait until content actually arrived in console. + * + * @param s content to write in output stream + * @param out use this output stream instead of default one + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #writeFast(String, IOConsoleOutputStream) + */ + public IOConsoleTestUtil write(final String s, IOConsoleOutputStream out) throws Exception { + writeFast(s, out); + waitForScheduledJobs(); + return this; + } + + /** + * Simulate {@link IOConsole} received input from connected processes output + * stream and verify new console content and partitioning. Expects written + * string was appended to end of previous console content and has not + * overwritten or removed any previous content. Further expects that + * partitioning of new written content is not mixed with other partition + * types. + * + * @param s content to write in output stream + * @param out use this output stream instead of default one + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil writeAndVerify(final String s, IOConsoleOutputStream out) throws Exception { + final int oldLength = doc.getLength(); + write(s, out); + TestCase.assertEquals("Console content length not as expected.", oldLength + s.length(), doc.getLength()); + verifyContentByOffset(s, oldLength); + verifyOutputPartitions(oldLength, s.length()); + return this; + } + + /** + * Simulate user input. This method is similar to user pasting some content + * in console but may not be equal in some case. Especially caret movement + * can be different in so use with care. + * + * @param content string to insert in console + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #insertTyping(String) + */ + public IOConsoleTestUtil insert(String content) { + final int oldCaretOffset = getCaretOffset(); + final int oldSelectionLength = textPanel.getSelectionCount(); + final Event e = new Event(); + e.start = oldCaretOffset - oldSelectionLength; + e.end = oldCaretOffset; + e.text = content; + e.doit = true; + textPanel.notifyListeners(SWT.Verify, e); + if (e.doit) { + textPanel.replaceTextRange(e.start, e.end - e.start, e.text); + // replace does not move caret itself but when a real user paste + // content the caret is updated + setCaretOffset(e.start + content.length()); + } + TestUtil.waitForJobs(name, 0, 1000); + return this; + } + + /** + * Simulate user input and verify the resulting content. This method is + * similar to user pasting some content in console but not equal in some + * case. Especially caret movement is different in some cases so use with + * care. + * <p> + * The verification expects the content is written at current caret offset, + * so do not use if caret is magically moved. (e.g. if insert starts in + * read-only part) + * </p> + * + * @param content string to insert in console + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #insertTypingAndVerify(String) + */ + public IOConsoleTestUtil insertAndVerify(String content) { + final int oldLength = doc.getLength(); + final int oldSelectionLength = textPanel.getSelectionCount(); + final int oldOffset = getCaretOffset() - oldSelectionLength; + insert(content); + TestCase.assertEquals("Console content length not as expected.", oldLength + content.length() - oldSelectionLength, doc.getLength()); + TestCase.assertEquals("Caret not at expected position.", oldOffset + content.length(), getCaretOffset()); + verifyContentByOffset(content, oldOffset); + verifyInputPartitions(oldOffset, content.length()); + return this; + } + + /** + * Simulate user typing some text in console. This method simulates + * keystrokes and therefore tests console behavior quite realistic. + * <p> + * Note: typing <code>\n</code> may result in platform dependent line + * delimiter entered. Also <code>\r\n</code> is typed as two separate + * characters and therefore leads to two line breaks. + * </p> + * + * @param content string to insert in console + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #insert(String) + */ + public IOConsoleTestUtil insertTyping(String content) { + for (char c : content.toCharArray()) { + final Event e = new Event(); + e.character = c; + textPanel.notifyListeners(SWT.KeyDown, e); + textPanel.notifyListeners(SWT.KeyUp, e); + } + TestUtil.waitForJobs(name, 0, 1000); + return this; + } + + /** + * Simulate user typing some text in console and check results. This method + * simulates keystrokes and therefore tests console behavior quite + * realistic. + * <p> + * Note: typing <code>\n</code> may result in platform dependent line + * delimiter entered. Also <code>\r\n</code> is typed as two separate + * characters and therefore leads to two line breaks. + * </p> + * + * @param content string to insert in console + * @return this {@link IOConsoleTestUtil} to chain methods + * @see #insertAndVerify(String) + */ + public IOConsoleTestUtil insertTypingAndVerify(String content) { + final int oldLength = doc.getLength(); + final int oldSelectionLength = textPanel.getSelectionCount(); + final int oldOffset = getCaretOffset() - oldSelectionLength; + insertTyping(content); + TestCase.assertEquals("Console content length not as expected.", oldLength + content.length() - oldSelectionLength, doc.getLength()); + TestCase.assertEquals("Caret not at expected position.", oldOffset + content.length(), getCaretOffset()); + verifyContentByOffset(content, oldOffset); + verifyInputPartitions(oldOffset, content.length()); + return this; + } + + /** + * Simulate user pressing backspace. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil backspace() { + return backspace(1); + } + + /** + * Simulate user pressing backspace multiple times. + * + * @param repeat perform this many backspaces + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil backspace(int repeat) { + for (int i = 0; i < repeat; i++) { + textPanel.invokeAction(ST.DELETE_PREVIOUS); + } + return this; + } + + /** + * Simulate user pressing enter. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil enter() { + // Note: newline will be replaced by + // System.getProperty("line.separator") so do not assume document length + // will increase by 1 + insertTyping("\n"); + return this; + } + + /** + * Current caret position in console. + * + * @return current caret position in console + * @see StyledText#getCaretOffset() + */ + public int getCaretOffset() { + return textPanel.getCaretOffset(); + } + + /** + * Set caret to new position. + * + * @param offset new caret position + * @return this {@link IOConsoleTestUtil} to chain methods + * @see StyledText#setCaretOffset(int) + */ + public IOConsoleTestUtil setCaretOffset(int offset) { + textPanel.setCaretOffset(offset); + return this; + } + + /** + * Move caret by given amount forth or back. + * + * @param amount steps to set caret forth (positive value) or back (negative + * value) + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil moveCaret(int amount) { + setCaretOffset(getCaretOffset() + amount); + return this; + } + + /** + * Move caret to start of console content. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil moveCaretToStart() { + textPanel.invokeAction(ST.TEXT_START); + return this; + } + + /** + * Move caret to end of console content. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil moveCaretToEnd() { + textPanel.invokeAction(ST.TEXT_END); + return this; + } + + /** + * Move caret to start of its current line. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil moveCaretToLineStart() { + textPanel.invokeAction(ST.LINE_START); + return this; + } + + /** + * Move caret to end of its current line. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil moveCaretToLineEnd() { + textPanel.invokeAction(ST.LINE_END); + return this; + } + + /** + * Select text in console. + * + * @param offset selection start position. May be negative to select + * relative from document end. + * @param length selection length + * @return this {@link IOConsoleTestUtil} to chain methods + * @see StyledText#setSelection(int, int) + */ + public IOConsoleTestUtil select(int offset, int length) { + final int o = offset < 0 ? doc.getLength() + offset : offset; + setCaretOffset(o + length); + textPanel.setSelectionRange(o, length); + return this; + } + + /** + * Check if console content equals the expected content. + * + * @param expectedContent content expect in console + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyContent(String expectedContent) { + verifyContentByOffset(expectedContent, 0); + TestCase.assertEquals("More or less content in console as expected.", expectedContent.length(), doc.getLength()); + return this; + } + + /** + * Check if line in console has expected content. + * + * @param expectedContent content expect in console + * @param lineNum the line in console to check. First line has number + * <code>0</code>. Accepts negative numbers as line relative to + * console end. + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyContentByLine(String expectedContent, int lineNum) { + final int l = lineNum < 0 ? doc.getNumberOfLines() + lineNum : lineNum; + try { + final IRegion line = doc.getLineInformation(l); + verifyContentByOffset(expectedContent, line.getOffset()); + TestCase.assertEquals("Line " + l + " has wrong length.", expectedContent.length(), line.getLength()); + } catch (BadLocationException e) { + TestCase.fail("Expected line not found in console document. Bad location!"); + } + return this; + } + + /** + * Check if console contains expected content at given offset. + * + * @param expectedContent content expect in console + * @param offset position where content is expected. If offset is negative + * it will be interpreted as relative to document end. + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyContentByOffset(String expectedContent, int offset) { + try { + final int o = offset < 0 ? doc.getLength() + offset : offset; + final int len = Math.min(doc.getLength() - o, expectedContent.length()); + TestCase.assertEquals("Expected string not found in console document.", expectedContent, doc.get(o, len)); + } catch (BadLocationException ex) { + TestCase.fail("Expected string not found in console document. Bad location!"); + } + return this; + } + + /** + * Check if console range is partitioned only by output partitions. + * + * @param offset range start + * @param length range length + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyOutputPartitions(int offset, int length) { + return verifyPartitions(offset, length, outputPartitionType(), false, false, 0); + } + + /** + * Check if console range is partitioned only by input partitions. + * + * @param offset range start + * @param length range length + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyInputPartitions(int offset, int length) { + return verifyPartitions(offset, length, inputPartitionType(), false, false, 0); + } + + /** + * Check if whole console document is partitioned. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyPartitions() { + return verifyPartitions(0); + } + + /** + * Check if whole console document is partitioned. + * + * @param minPartitionNumber the minimum number of partitions expected + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil verifyPartitions(int minPartitionNumber) { + return verifyPartitions(0, doc.getLength(), null, true, false, minPartitionNumber); + } + + /** + * Check partitioning of console range. + * + * @param offset range start + * @param length range length + * @param expectedType either all partitions in checked ranges are expected + * to be of this type or <code>null</code> to not force type + * @param allowMixedTypes if true range may comprised from all partition + * types. If false all partitions in range must be of same type. + * @param allowGaps if false + * {@link IDocumentPartitioner#computePartitioning(int, int)} + * must not contain any unpartitioned parts + * @param minPartitionNumber the minimum number of partitions expected + * @return this {@link IOConsoleTestUtil} to chain methods + * @see IDocumentPartitioner#computePartitioning(int, int) + * @see IDocumentPartitioner#getPartition(int) + */ + public IOConsoleTestUtil verifyPartitions(final int offset, final int length, final String expectedType, final boolean allowMixedTypes, final boolean allowGaps, int minPartitionNumber) { + { + final ITypedRegion[] partitions = getPartitioner().computePartitioning(offset, length); + TestCase.assertNotNull("Got no partitions.", partitions); + TestCase.assertTrue("Got less partitions than expected.", partitions.length >= minPartitionNumber); + + // points to offset after last partition range aka. expected start + // of next partition + int lastEnd = -1; + ITypedRegion lastPartition = null; + String partitionType = expectedType; + for (ITypedRegion partition : partitions) { + TestCase.assertNotSame("Got same partition twice.", lastPartition, partition); + TestCase.assertTrue("Partition overlapping. (or not sorted as expected)", partition.getOffset() >= lastEnd); + if (!allowGaps && lastEnd != -1) { + TestCase.assertTrue("Partitioning gap.", partition.getOffset() == lastEnd); + } + TestCase.assertTrue("Not a valid partition type.", validPartionTypes.contains(partition.getType())); + TestCase.assertTrue("Wrong partition type.", partitionType == null || partitionType.equals(partition.getType()) || allowMixedTypes); + if (partitionType == null && !allowMixedTypes) { + partitionType = partition.getType(); + } + lastEnd = partition.getOffset() + partition.getLength(); + lastPartition = partition; + } + } + { + int pos = offset; + int end = offset + length; + ITypedRegion lastPartition = null; + String partitionType = expectedType; + while (pos < end) { + final ITypedRegion partition = getPartitioner().getPartition(pos); + if (partition == null) { + TestCase.assertTrue("Did not expect 'null' partition.", allowGaps); + pos++; + continue; + } + TestCase.assertNotSame("Got same partition again.", lastPartition, partition); + TestCase.assertFalse("Did not expected and cannot handle empty partition.", partition.getLength() == 0); + TestCase.assertTrue("Got not the requested partition.", partition.getOffset() <= pos && partition.getOffset() + partition.getLength() > pos); + TestCase.assertTrue("Not a valid partition type.", validPartionTypes.contains(partition.getType())); + lastPartition = partition; + if (partitionType != null && !partitionType.equals(partition.getType())) { + TestCase.assertTrue("Wrong partition type.", allowMixedTypes); + pos += partition.getLength(); + end += partition.getLength(); + continue; + } + if (partitionType == null && !allowMixedTypes) { + partitionType = partition.getType(); + } + pos = partition.getOffset() + partition.getLength(); + } + } + return this; + } + + /** + * Wait for waiting or scheduled jobs. + * + * @return this {@link IOConsoleTestUtil} to chain methods + */ + public IOConsoleTestUtil waitForScheduledJobs() { + boolean jobSleeping = false; + for (int i = 0; i < 5 && !jobSleeping; i++) { + final boolean jobPending = TestUtil.waitForJobs(name, 25, 2000); + if (!jobPending) { + jobSleeping = false; + for (Job job : TestUtil.getRunningOrWaitingJobs(null)) { + if (job.getState() == Job.SLEEPING) { + jobSleeping = true; + break; + } + } + } + } + return this; + } + + /** + * Close the default output stream if it was used. + */ + public void closeOutputStream() { + if (defaultOut != null) { + try { + defaultOut.close(); + } catch (IOException e) { + TestCase.fail("Failed to close output stream."); + } + } + } + + /** + * Get length of current console content. + * + * @return consoles document length + */ + public int getContentLength() { + return getDocument().getLength(); + } + + public IOConsole getConsole() { + return console; + } + + public IDocument getDocument() { + return doc; + } + + public IConsoleDocumentPartitioner getPartitioner() { + return partitioner; + } + + public IOConsoleOutputStream getDefaultOutputStream() { + if (defaultOut == null) { + defaultOut = console.newOutputStream(); + } + return defaultOut; + } + + /** + * Get identifier for output {@link IOConsolePartition}s. + * + * @return output partition identifier + */ + @SuppressWarnings("restriction") + public static String outputPartitionType() { + return org.eclipse.ui.internal.console.IOConsolePartition.OUTPUT_PARTITION_TYPE; + } + + /** + * Get identifier for input {@link IOConsolePartition}s. + * + * @return input partition identifier + */ + @SuppressWarnings("restriction") + public static String inputPartitionType() { + return org.eclipse.ui.internal.console.IOConsolePartition.INPUT_PARTITION_TYPE; + } +} diff --git a/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java new file mode 100644 index 000000000..b82bfb3a7 --- /dev/null +++ b/org.eclipse.debug.tests/src/org/eclipse/debug/tests/console/IOConsoleTests.java @@ -0,0 +1,332 @@ +/******************************************************************************* + * Copyright (c) 2019 Paul Pazderski and others. + * + * 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 + * + * Contributors: + * Paul Pazderski - initial API and implementation + *******************************************************************************/ +package org.eclipse.debug.tests.console; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.core.runtime.ILogListener; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; +import org.eclipse.debug.tests.AbstractDebugTest; +import org.eclipse.debug.tests.TestUtil; +import org.eclipse.debug.tests.TestsPlugin; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.console.ConsolePlugin; +import org.eclipse.ui.console.IConsole; +import org.eclipse.ui.console.IConsoleConstants; +import org.eclipse.ui.console.IConsoleManager; +import org.eclipse.ui.console.IOConsole; +import org.eclipse.ui.console.IOConsoleOutputStream; + +/** + * Tests the {@link IOConsole}. Especially the partitioner and viewer parts. + */ +public class IOConsoleTests extends AbstractDebugTest { + /** + * The console view used for the running test. Required to obtain access to + * consoles {@link StyledText} widget to simulate user input. + */ + @SuppressWarnings("restriction") + private org.eclipse.ui.internal.console.ConsoleView consoleView; + + /** Track console finished property notification. */ + private final AtomicBoolean consoleFinished = new AtomicBoolean(false); + + /** + * Number of received log messages with severity error while running a + * single test method. + */ + private final AtomicInteger loggedErrors = new AtomicInteger(); + + /** Listener to count error messages while testing. */ + private final ILogListener errorLogListener = (IStatus status, String plugin) -> { + if (status.matches(IStatus.ERROR)) { + loggedErrors.incrementAndGet(); + } + }; + + public IOConsoleTests() { + super(IOConsoleTests.class.getSimpleName()); + } + + public IOConsoleTests(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // create or activate console view + final IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + assertNotNull(window); + final IWorkbenchPage activePage = window.getActivePage(); + assertNotNull(activePage); + IViewPart viewPart = activePage.findView(IConsoleConstants.ID_CONSOLE_VIEW); + if (viewPart == null) { + viewPart = activePage.showView(IConsoleConstants.ID_CONSOLE_VIEW, null, IWorkbenchPage.VIEW_CREATE); + } + @SuppressWarnings("restriction") + final org.eclipse.ui.internal.console.ConsoleView castConsoleView = (org.eclipse.ui.internal.console.ConsoleView) viewPart; + consoleView = castConsoleView; + activePage.activate(consoleView); + + // add error listener + loggedErrors.set(0); + Platform.addLogListener(errorLogListener); + } + + @Override + protected void tearDown() throws Exception { + assertEquals("Test triggered errors in IOConsole.", 0, loggedErrors.get()); + Platform.removeLogListener(errorLogListener); + super.tearDown(); + } + + /** + * Create test util connected to a new {@link IOConsole}. + * + * @param title console title + * @return util to help testing console functions + */ + private IOConsoleTestUtil getTestUtil(String title) { + final IOConsole console = new IOConsole(title, "", null, StandardCharsets.UTF_8.name(), true); + consoleFinished.set(false); + console.addPropertyChangeListener((PropertyChangeEvent event) -> { + if (event.getSource() == console && IConsoleConstants.P_CONSOLE_OUTPUT_COMPLETE.equals(event.getProperty())) { + consoleFinished.set(true); + } + }); + final IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + consoleManager.addConsoles(new IConsole[] { console }); + TestUtil.waitForJobs(getName(), 25, 10000); + consoleManager.showConsoleView(console); + @SuppressWarnings("restriction") + final org.eclipse.ui.internal.console.IOConsolePage page = (org.eclipse.ui.internal.console.IOConsolePage) consoleView.getCurrentPage(); + final StyledText textPanel = (StyledText) page.getControl(); + return new IOConsoleTestUtil(console, textPanel, getName()); + } + + /** + * Close the console and optionally check content in {@link IOConsole}'s + * input stream. + * <p> + * Note: all output streams explicitly opened with + * {@link IOConsole#newOutputStream()} must be closed before. + * </p> + * + * @param c the test util containing the console to close + * @param expected content this {@link IOConsole} input stream has received + */ + private void closeConsole(IOConsoleTestUtil c, String... expectedInputLines) throws IOException { + if (consoleFinished.get()) { + // This should only happen if no output streams where used and the + // user input stream was explicit closed before + TestUtil.log(IStatus.WARNING, TestsPlugin.PLUGIN_ID, "Console was finished before streams where explicit closed."); + } + + c.closeOutputStream(); + if (c.getConsole().getInputStream() != null) { + c.getConsole().getInputStream().close(); + } + + if (expectedInputLines.length > 0) { + assertNotNull(c.getConsole().getInputStream()); + assertTrue("InputStream is empty.", c.getConsole().getInputStream().available() > 0); + + final List<String> inputLines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(c.getConsole().getInputStream(), c.getConsole().getCharset()))) { + String line; + while (reader.ready() && (line = reader.readLine()) != null) { + inputLines.add(line); + } + } + assertEquals("Input contains to many/few lines.", expectedInputLines.length, inputLines.size()); + for (int i = 0; i < expectedInputLines.length; i++) { + assertEquals("Content of input line " + i + " not as expected.", expectedInputLines[i], inputLines.get(i)); + } + } + c.waitForScheduledJobs(); + assertTrue("Console close was not signaled.", consoleFinished.get()); + + final IConsoleManager consoleManager = ConsolePlugin.getDefault().getConsoleManager(); + consoleManager.removeConsoles(new IConsole[] { c.getConsole() }); + } + + /** + * Test console clear. + */ + public void testConsoleClear() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test clear"); + c.writeAndVerify("New console content."); + c.clear(); + closeConsole(c); + } + + /** + * Test multiple writes, i.e. {@link IOConsole} receives content from + * connected {@link IOConsoleOutputStream}. + */ + public void testWrites() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test writes"); + final String[] strings = new String[] { + "Hello ", "World", "foo-", "bar", "123 456" }; + for (String s : strings) { + c.writeAndVerify(s); + } + c.write("\n"); + final String longString = String.join("", Collections.nCopies(1000, "0123456789")); + c.writeAndVerify(longString); + c.verifyContentByOffset(strings[1], strings[0].length()); + c.verifyContentByLine(String.join("", strings), 0); + c.verifyPartitions(); + closeConsole(c); + } + + /** + * Test {@link IOConsole} input stream, i.e. simulate user typing or pasting + * input in console. + */ + public void testUserInput() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test input"); + final List<String> expectedInput = new ArrayList<>(); + + c.insertAndVerify("remove").select(0, c.getContentLength()).verifyPartitions(); + c.insertTypingAndVerify("abc").insertAndVerify("123").verifyContent("abc123"); + c.moveCaret(-3).insertAndVerify("foo").insertTypingAndVerify("bar").verifyContentByOffset("123", c.getCaretOffset()); + c.moveCaretToLineEnd().backspace(2).verifyContent("abcfoobar1").verifyPartitions(); + c.insert("\r\n").backspace(5); + expectedInput.add("abcfoobar1"); + int pos = c.getCaretOffset(); + c.insertTypingAndVerify("NewLine").moveCaret(-4).enter(); + expectedInput.add("NewLine"); + assertEquals("Expected newline entered inside line does not break this line.", c.getContentLength(), c.getCaretOffset()); + c.verifyPartitions().verifyContentByOffset("NewLine", pos); + c.backspace().insertAndVerify("--").select(0, c.getContentLength()).insertTyping("<~>"); + c.verifyContentByLine("--<~>", 2).verifyPartitions(); + c.select(-2, 1).insertAndVerify("-=-").verifyContentByLine("--<-=->", 2).verifyPartitions(); + + closeConsole(c, expectedInput.toArray(new String[0])); + } + + /** + * Test mixes of outputs and user inputs in various variants. + */ + public void testMixedWriteAndInput() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test input output mix"); + final List<String> expectedInput = new ArrayList<>(); + + // user input mixed with outputs without caret movement + c.writeAndVerify("foo"); + c.insertTyping("~~~"); + c.writeAndVerify("bar"); + c.insertTyping("input."); + c.verifyContent("foo~~~barinput.").verifyPartitions(3); + c.enter().clear(); + expectedInput.add("~~~input."); + + // type in output or finished input partitions and replace input + c.insert("fixed\n"); + expectedInput.add("fixed"); + c.setCaretOffset(2); + c.insertTyping("+"); + c.writeAndVerify("out"); + c.moveCaretToEnd().moveCaret(-1); + c.insert("more input"); + c.select(0, c.getContentLength()).backspace(); + c.insert("~~p#t").select(c.getContentLength() - 5, 2).insert("in"); + c.select(c.getContentLength() - 2, 1).insertTyping("u"); + c.enter().clear(); + expectedInput.add("+more inputinput"); + + closeConsole(c, expectedInput.toArray(new String[0])); + } + + /** + * Test larger number of partitions with pseudo random console content. + */ + public void testManyPartitions() throws IOException { + final IOConsoleTestUtil c = getTestUtil("Test many partitions"); + final List<String> expectedInput = new ArrayList<>(); + final StringBuilder input = new StringBuilder(); + try (IOConsoleOutputStream otherOut = c.getConsole().newOutputStream()) { + final Random rand = new Random(12); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 20; i++) { + for (int j = 0; j < 80; j += sb.length()) { + sb.setLength(0); + for (int k = rand.nextInt(15) + 1; k > 0; k--) { + // add printable ASCII character + sb.append((char) (rand.nextInt(95) + 32)); + } + switch (rand.nextInt(5)) { + case 0: + c.insert(sb.toString()); + input.append(sb); + break; + + case 1: + case 2: + c.writeFast(sb.toString(), otherOut); + break; + + default: + c.writeFast(sb.toString()); + break; + } + } + c.enter().verifyPartitions(); + expectedInput.add(input.toString()); + input.setLength(0); + } + } + closeConsole(c, expectedInput.toArray(new String[0])); + } + + /** + * Test console trimming. + */ + public void testTrim() throws Exception { + final IOConsoleTestUtil c = getTestUtil("Test trim"); + try (IOConsoleOutputStream otherOut = c.getConsole().newOutputStream()) { + c.writeFast("first\n"); + for (int i = 0; i < 20; i++) { + c.writeFast("0123456789\n", (i & 1) == 0 ? c.getDefaultOutputStream() : otherOut); + } + c.write("last\n"); + c.verifyContentByLine("first", 0).verifyContentByLine("last", -2); + assertTrue("Document not filled.", c.getDocument().getNumberOfLines() > 15); + + c.getConsole().setWaterMarks(50, 100); + c.waitForScheduledJobs(); + c.verifyContentByOffset("0123456789", 0); + assertTrue("Document not trimmed.", c.getDocument().getNumberOfLines() < 15); + } + closeConsole(c); + } +} |