blob: 1ee0b76183ee598206bccdd374e21a8aa18fd96b [file] [log] [blame]
/*
* Copyright 2000-2011 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.execution.testframework.sm.runner;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.util.Key;
import gnu.trove.THashMap;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public abstract class OutputLineSplitter {
private static final String TEAMCITY_SERVICE_MESSAGE_PREFIX = "##teamcity[";
private final boolean myStdinSupportEnabled;
private final Map<Key, StringBuilder> myBuffers = new THashMap<Key, StringBuilder>();
private final List<OutputChunk> myStdOutChunks = new ArrayList<OutputChunk>();
public OutputLineSplitter(boolean stdinEnabled) {
myBuffers.put(ProcessOutputTypes.SYSTEM, new StringBuilder());
myBuffers.put(ProcessOutputTypes.STDERR, new StringBuilder());
myStdinSupportEnabled = stdinEnabled;
}
public void process(final String text, final Key outputType) {
int from = 0;
int to = 0;
for (; to < text.length(); to++) {
if (text.charAt(to) == '\n') {
processLine(text.substring(from, to + 1), outputType);
from = to + 1;
}
}
if (from < text.length()) {
processLine(text.substring(from), outputType);
}
}
private void processLine(String text, Key outputType) {
if (!myBuffers.keySet().contains(outputType)) {
processStdOutConsistently(text, outputType);
}
else {
StringBuilder buffer = myBuffers.get(outputType);
if (!text.endsWith("\n")) {
buffer.append(text);
return;
}
if (buffer.length() > 0) {
buffer.append(text);
text = buffer.toString();
buffer.setLength(0);
}
onLineAvailable(text, outputType, false);
}
}
private void processStdOutConsistently(final String text, final Key outputType) {
final int textLength = text.length();
if (textLength == 0) {
return;
}
synchronized (myStdOutChunks) {
myStdOutChunks.add(new OutputChunk(outputType, text));
}
final char lastChar = text.charAt(textLength - 1);
if (lastChar == '\n' || lastChar == '\r') {
// buffer contains consistent string
flushStdOutBuffer();
}
else {
// test framework may show some promt and ask user for smth. Question may not
// finish with \n or \r thus buffer wont be flushed and user will have to input smth
// before question. And question will became visible with next portion of text.
// Such behaviour is confusing. So
// 1. Let's assume that sevice messages starts with \n if console is editable
// 2. Then we can suggest that each service message will start from new line and buffer should
// be flushed before every service message. Thus if chunks list is empty and output doesn't end
// with \n or \r but starts with ##teamcity then it is a service message and should be buffered otherwise
// we can safely flush buffer.
// TODO if editable:
if (myStdinSupportEnabled && !isMostLikelyServiceMessagePart(text)) {
flushStdOutBuffer();
}
}
}
private void flushStdOutBuffer() {
// if osColoredProcessHandler was attached it can split string with several colors
// in several parts. Thus '\n' symbol may be send as one part with some color
// such situation should differ from single '\n' from process that is used by TC reporters
// to separate TC commands from other stuff + optimize flushing
// TODO: probably in IDEA mode such runners shouldn't add explicit \n because we can
// successfully process broken messages across several flushes
// size of parts may tell us either \n was single in original flushed data or it was
// separated by process handler
List<OutputChunk> chunks = new ArrayList<OutputChunk>();
OutputChunk lastChunk = null;
synchronized (myStdOutChunks) {
for (OutputChunk chunk : myStdOutChunks) {
if (lastChunk != null && chunk.getKey() == lastChunk.getKey()) {
lastChunk.append(chunk.getText());
}
else {
lastChunk = chunk;
chunks.add(chunk);
}
}
myStdOutChunks.clear();
}
final boolean isTCLikeFakeOutput = chunks.size() == 1;
for (OutputChunk chunk : chunks) {
onLineAvailable(chunk.getText(), chunk.getKey(), isTCLikeFakeOutput);
}
}
public void flush() {
flushStdOutBuffer();
for (Map.Entry<Key, StringBuilder> each : myBuffers.entrySet()) {
StringBuilder buffer = each.getValue();
if (buffer.length() > 0) {
onLineAvailable(buffer.toString(), each.getKey(), false);
buffer.setLength(0);
}
}
}
protected boolean isMostLikelyServiceMessagePart(@NotNull final String text) {
return text.startsWith(TEAMCITY_SERVICE_MESSAGE_PREFIX);
}
protected abstract void onLineAvailable(@NotNull String text, @NotNull Key outputType, boolean tcLikeFakeOutput);
private static class OutputChunk {
private final Key myKey;
private String myText;
private OutputChunk(Key key, String text) {
myKey = key;
myText = text;
}
public Key getKey() {
return myKey;
}
public String getText() {
return myText;
}
public void append(String text) {
myText += text;
}
}
}