From f4ed78c8f574081aa251e9be15bd6920c77fdef5 Mon Sep 17 00:00:00 2001 From: "Ilya.Kazakevich" Date: Fri, 28 Jun 2019 21:28:41 +0300 Subject: [PATCH] WI-46570: Do not break long TC message attributes. * There should be one place to cut long messages, no need several places * One can't simply cut part of TC service message. We can only cut part of one of it's values So, we parse message, look for longest attribute value and cut this value. Message is valid and consistent that way. GitOrigin-RevId: bc5a0142e03483a1566e158e94fe808b054919a4 --- .../testframework/sm/runner/LongLineCutter.kt | 72 ++++++++++++++++ .../sm/runner/OutputEventSplitter.kt | 15 +--- .../OutputToGeneralTestEventsConverter.java | 11 +-- .../testframework/sm/LongLineCutterTest.kt | 86 +++++++++++++++++++ .../sm/OutputLineSplitterTest.java | 28 ++++++ 5 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 platform/smRunner/src/com/intellij/execution/testframework/sm/runner/LongLineCutter.kt create mode 100644 platform/smRunner/testSrc/com/intellij/execution/testframework/sm/LongLineCutterTest.kt 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##";