diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/LongLineCutter.kt b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/LongLineCutter.kt new file mode 100644 index 000000000000..d432e8859303 --- /dev/null +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/LongLineCutter.kt @@ -0,0 +1,72 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.execution.testframework.sm.runner + +import com.intellij.execution.impl.ConsoleBuffer +import jetbrains.buildServer.messages.serviceMessages.ServiceMessage + +private const val ELLIPSIS = "<...>" + +private val FIELDS_NOT_TO_TOUCH = setOf("name", "duration", "type", "flowId", "nodeId", "parentNodeId") +private val EXPECTED_ACTUAL = arrayOf("expected", "actual") + + +/** + * If [text] is longer than [maxLength] cut it and insert [ELLIPSIS] in cut. + * If it is a [ServiceMessage], then find longest attribute and cut it leaving [margin] as prefix and postfix + * [EXPECTED_ACTUAL] attrs both are cut. + * + */ +fun cutLineIfTooLong(text: String, maxLength: Int = ConsoleBuffer.getCycleBufferSize(), margin: Int = 1000): String { + val minValueLengthToCut = (margin * 2) + ELLIPSIS.length + if (text.length <= maxLength || maxLength < minValueLengthToCut) { + return text + } + + val message = ServiceMessage.parse(text.trim()) + + if (message == null) { + //Not a message, cut as regular text + return text.substring(0, maxLength - ELLIPSIS.length) + ELLIPSIS + } + + val attributes = HashMap(message.attributes) + val attributesToCut = attributes + .filter { it.key !in FIELDS_NOT_TO_TOUCH } + .toList() + .sortedByDescending { it.second.length } + .map { it.first } + + val shortener = Shortener(attributes, text.length, margin, minValueLengthToCut) + for (attr in attributesToCut) { + if (shortener.currentLength < maxLength) { + break + } + shortener.shortenAttribute(attr) + if (attr in EXPECTED_ACTUAL) { + EXPECTED_ACTUAL.forEach(shortener::shortenAttribute) + } + } + return ServiceMessage.asString(message.messageName, attributes) +} + +private class Shortener(private val attributes: MutableMap, + var currentLength: Int, + private val minValueLengthToCut: Int, + private val margin: Int) { + private val shortened = mutableSetOf() + fun shortenAttribute(attribute: String) { + if (attribute in shortened) { + return + } + val value = attributes[attribute] ?: return + if (value.length <= minValueLengthToCut) { // Tool short to cut + return + } + + val lenBefore = value.length + val newValue = StringBuilder(value).replace(margin, value.length - margin, ELLIPSIS).toString() + currentLength -= (lenBefore - newValue.length) + attributes[attribute] = newValue + shortened.add(attribute) + } +} diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputEventSplitter.kt b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputEventSplitter.kt index 2655c3a21a77..54dab6202235 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputEventSplitter.kt +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputEventSplitter.kt @@ -34,7 +34,6 @@ import java.util.concurrent.atomic.AtomicReference */ abstract class OutputEventSplitter(private val bufferTextUntilNewLine: Boolean = false) { - private val currentCyclicBufferSize = ConsoleBuffer.getCycleBufferSize() private val prevRefs: Map> = listOf(ProcessOutputType.STDOUT, ProcessOutputType.STDERR, ProcessOutputType.SYSTEM) .map { it to AtomicReference() }.toMap() @@ -146,21 +145,11 @@ abstract class OutputEventSplitter(private val bufferTextUntilNewLine: Boolean = } private fun flushInternal(text: String, key: Key<*>) { - var result = text - // Cut long lines - if (USE_CYCLE_BUFFER && - text.length > currentCyclicBufferSize && - currentCyclicBufferSize > 2 * SM_MESSAGE_PREFIX) { - result = text.substring(0, SM_MESSAGE_PREFIX) + text.substring(text.length - SM_MESSAGE_PREFIX) - } - - onTextAvailable(result, key) + val textToAdd = if (USE_CYCLE_BUFFER) cutLineIfTooLong(text) else text + onTextAvailable(textToAdd, key) } } - -internal const val SM_MESSAGE_PREFIX = 105 - private val USE_CYCLE_BUFFER = ConsoleBuffer.useCycleBuffer() private const val SERVICE_MESSAGE_START: String = ServiceMessage.SERVICE_MESSAGE_START private const val NEW_LINE: Char = '\n' diff --git a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java index 34e3bb272257..4cbeef63ecd7 100644 --- a/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java +++ b/platform/smRunner/src/com/intellij/execution/testframework/sm/runner/OutputToGeneralTestEventsConverter.java @@ -1,7 +1,6 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.execution.testframework.sm.runner; -import com.intellij.execution.impl.ConsoleBuffer; import com.intellij.execution.process.ColoredOutputTypeRegistry; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.testframework.TestConsoleProperties; @@ -31,7 +30,6 @@ import static com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerE */ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer { private static final Logger LOG = Logger.getInstance(OutputToGeneralTestEventsConverter.class.getName()); - private static final boolean USE_CYCLE_BUFFER = ConsoleBuffer.useCycleBuffer(); private final MyServiceMessageVisitor myServiceMessageVisitor; private final String myTestFrameworkName; @@ -40,8 +38,7 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer private volatile GeneralTestEventsProcessor myProcessor; private Runnable myTestingStartedHandler; private boolean myFirstTestingStartedEvent = true; - private static final String ELLIPSIS = "<...>"; - private final int myCycleBufferSize = ConsoleBuffer.getCycleBufferSize(); + public OutputToGeneralTestEventsConverter(@NotNull final String testFrameworkName, @NotNull final TestConsoleProperties consoleProperties) { // If console is editable, user may want to see output before new line char. @@ -103,12 +100,6 @@ public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer } protected void processConsistentText(@NotNull String text, @NotNull final Key outputType) { - if (USE_CYCLE_BUFFER && text.length() > myCycleBufferSize && myCycleBufferSize > OutputEventSplitterKt.SM_MESSAGE_PREFIX) { - text = text.substring(0, myCycleBufferSize - OutputEventSplitterKt.SM_MESSAGE_PREFIX) + - ELLIPSIS + - text.substring(text.length() - OutputEventSplitterKt.SM_MESSAGE_PREFIX + ELLIPSIS.length()); - } - try { if (!processServiceMessages(text, outputType, myServiceMessageVisitor)) { //fire current output diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/LongLineCutterTest.kt b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/LongLineCutterTest.kt new file mode 100644 index 000000000000..fa8a71b60436 --- /dev/null +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/LongLineCutterTest.kt @@ -0,0 +1,86 @@ +// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.execution.testframework.sm + +import com.intellij.execution.testframework.sm.runner.cutLineIfTooLong +import jetbrains.buildServer.messages.serviceMessages.ServiceMessage +import org.junit.Assert +import org.junit.Test + + +class LongLineCutterTest { + @Test + fun shortMessageUntouched() { + val message = ServiceMessage.asString("foo", mapOf( + "A" to "B", + "Z" to "Q" + )) + Assert.assertEquals(message, cutLineIfTooLong(message, Int.MAX_VALUE, 100)) + } + + @Test + fun longLineShortened() { + val maxLength = 10000 + val text = cutLineIfTooLong("abcde".repeat(maxLength), maxLength, 100) + Assert.assertEquals(text.length, maxLength) + } + + @Test + fun actualExpectedShort() { + val maxLength = 1000 + val message = ServiceMessage.asString("testFailed", mapOf( + "expected" to "A".repeat(maxLength * 2), + "actual" to "B" + )); + val result = ServiceMessage.parse(cutLineIfTooLong(message, maxLength, 10))!! + + val actual = result.attributes["actual"]!! + val expected = result.attributes["expected"]!! + + Assert.assertEquals(actual, "B") + Assert.assertTrue(expected.startsWith("A")) + Assert.assertTrue(expected.endsWith("A")) + Assert.assertTrue("..." in expected) + + } + + @Test + fun actualExpectedLong() { + val maxLength = 1000 + val message = ServiceMessage.asString("testFailed", mapOf( + "expected" to "A".repeat(maxLength * 2), + "actual" to "B".repeat(maxLength * 2) + )); + val result = ServiceMessage.parse(cutLineIfTooLong(message, maxLength, 10))!! + + val actual = result.attributes["actual"]!! + val expected = result.attributes["expected"]!! + + Assert.assertTrue(actual.startsWith("B")) + Assert.assertTrue(actual.endsWith("B")) + Assert.assertTrue(expected.startsWith("A")) + Assert.assertTrue(expected.endsWith("A")) + Assert.assertTrue(expected.length == actual.length) + Assert.assertTrue("..." in actual) + Assert.assertTrue("..." in expected) + + } + + @Test + fun longMessageShortened() { + val maxLength = 10000 + val s = "abc\r\n" + val longString = s.repeat(maxLength * 2) + val message = ServiceMessage.asString("foo", mapOf( + "A" to "B", + "C" to "D", + "Z" to longString + )) + val result = cutLineIfTooLong(message, maxLength, 100) + Assert.assertTrue("Failed to cut message", result.length <= maxLength) + val shortenedMessage = ServiceMessage.parse(result)!! + Assert.assertEquals("B", shortenedMessage.attributes["A"]) + Assert.assertEquals("D", shortenedMessage.attributes["C"]) + val longestValue = shortenedMessage.attributes["Z"]!! + Assert.assertTrue("D", longestValue.startsWith(s) && longestValue.endsWith(s)) + } +} \ No newline at end of file diff --git a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/OutputLineSplitterTest.java b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/OutputLineSplitterTest.java index 0866421d0c93..e5411e1f559c 100644 --- a/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/OutputLineSplitterTest.java +++ b/platform/smRunner/testSrc/com/intellij/execution/testframework/sm/OutputLineSplitterTest.java @@ -15,6 +15,7 @@ */ package com.intellij.execution.testframework.sm; +import com.intellij.execution.impl.ConsoleBuffer; import com.intellij.execution.process.ProcessOutputType; import com.intellij.execution.process.ProcessOutputTypes; import com.intellij.execution.testframework.sm.runner.OutputEventSplitter; @@ -30,6 +31,7 @@ import org.hamcrest.core.IsCollectionContaining; import org.jetbrains.annotations.NotNull; import org.junit.Assert; +import java.text.ParseException; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; @@ -72,6 +74,32 @@ public class OutputLineSplitterTest extends LightPlatformTestCase { }; } + public void testLongText() { + final int maxSize = ConsoleBuffer.getCycleBufferSize(); + final String string = "abc"; + final String longString = StringUtil.repeat(string, maxSize); + mySplitter.process(longString, ProcessOutputType.STDOUT); + final String shortenedLine = myOutput.get(ProcessOutputTypes.STDOUT).toList().get(0); + Assert.assertEquals(shortenedLine.length(), maxSize); + Assert.assertTrue(shortenedLine.startsWith(string)); + } + + public void testLongMessage() throws ParseException { + final int maxSize = ConsoleBuffer.getCycleBufferSize(); + final String string = "abc|n"; + final String longString = StringUtil.repeat(string, maxSize); + final String junk = "QWE"; + final String message = + String.format("##teamcity[testFailed name='someTest' expected='%s' actual='%s']\n", longString, longString.replaceFirst("abc", junk)); + + mySplitter.process(message, ProcessOutputType.STDOUT); + final String shortenedLine = myOutput.get(ProcessOutputTypes.STDOUT).toList().get(0); + final ServiceMessage shortenedMessage = ServiceMessage.parse(shortenedLine); + Assert.assertTrue("Failed to shorten message", shortenedMessage.toString().length() <= maxSize); + final Map attrs = shortenedMessage.getAttributes(); + Assert.assertEquals(attrs.get("expected").replaceFirst("abc", junk), attrs.get("actual")); + } + public void testMessageEndFlush() { final String text = "hello##";