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
This commit is contained in:
Ilya.Kazakevich
2019-07-02 06:52:16 +03:00
committed by intellij-monorepo-bot
parent a724467ad1
commit f4ed78c8f5
5 changed files with 189 additions and 23 deletions
@@ -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<String, String>,
var currentLength: Int,
private val minValueLengthToCut: Int,
private val margin: Int) {
private val shortened = mutableSetOf<String>()
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)
}
}
@@ -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<ProcessOutputType, AtomicReference<Output>> =
listOf(ProcessOutputType.STDOUT, ProcessOutputType.STDERR, ProcessOutputType.SYSTEM)
.map { it to AtomicReference<Output>() }.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'
@@ -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
@@ -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))
}
}
@@ -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<String, String> attrs = shortenedMessage.getAttributes();
Assert.assertEquals(attrs.get("expected").replaceFirst("abc", junk), attrs.get("actual"));
}
public void testMessageEndFlush() {
final String text = "hello##";