| /* |
| * Copyright 2000-2009 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.execution.testframework.TestConsoleProperties; |
| import com.intellij.execution.testframework.sm.runner.events.*; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Key; |
| import com.intellij.openapi.util.text.StringUtil; |
| import jetbrains.buildServer.messages.serviceMessages.*; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.text.ParseException; |
| import java.util.Map; |
| |
| import static com.intellij.execution.testframework.sm.runner.GeneralToSMTRunnerEventsConvertor.getTFrameworkPrefix; |
| |
| /** |
| * @author Roman Chernyatchik |
| * <p/> |
| * This implementation also supports messages splitted in parts by early flush. |
| * Implementation assumes that buffer is being flushed on line end or by timer, |
| * i.e. incomming text contains no more than one line's end marker ('\r', '\n', or "\r\n") |
| * (e.g. process was run with IDEA program's runner) |
| */ |
| public class OutputToGeneralTestEventsConverter implements ProcessOutputConsumer { |
| private static final Logger LOG = Logger.getInstance(OutputToGeneralTestEventsConverter.class.getName()); |
| |
| private GeneralTestEventsProcessor myProcessor; |
| private final MyServiceMessageVisitor myServiceMessageVisitor; |
| private final String myTestFrameworkName; |
| |
| private final OutputLineSplitter mySplitter; |
| private boolean myPendingLineBreakFlag; |
| |
| public OutputToGeneralTestEventsConverter(@NotNull final String testFrameworkName, |
| @NotNull final TestConsoleProperties consoleProperties) { |
| myTestFrameworkName = testFrameworkName; |
| myServiceMessageVisitor = new MyServiceMessageVisitor(); |
| |
| mySplitter = new OutputLineSplitter(consoleProperties.isEditable()) { |
| @Override |
| protected void onLineAvailable(@NotNull String text, @NotNull Key outputType, boolean tcLikeFakeOutput) { |
| processConsistentText(text, outputType, tcLikeFakeOutput); |
| } |
| }; |
| } |
| |
| public void setProcessor(@Nullable final GeneralTestEventsProcessor processor) { |
| myProcessor = processor; |
| } |
| |
| public void dispose() { |
| setProcessor(null); |
| } |
| |
| public void process(final String text, final Key outputType) { |
| mySplitter.process(text, outputType); |
| } |
| |
| /** |
| * Flashes the rest of stdout text buffer after output has been stopped |
| */ |
| public void flushBufferBeforeTerminating() { |
| mySplitter.flush(); |
| if (myPendingLineBreakFlag) { |
| fireOnUncapturedLineBreak(); |
| } |
| } |
| |
| private void fireOnUncapturedLineBreak() { |
| fireOnUncapturedOutput("\n", ProcessOutputTypes.STDOUT); |
| } |
| |
| private void processConsistentText(final String text, final Key outputType, boolean tcLikeFakeOutput) { |
| try { |
| if (!processServiceMessages(text, outputType, myServiceMessageVisitor)) { |
| if (myPendingLineBreakFlag) { |
| // output type for line break isn't important |
| // we may use any, e.g. current one |
| fireOnUncapturedLineBreak(); |
| myPendingLineBreakFlag = false; |
| } |
| // Filters \n |
| String outputToProcess = text; |
| if (tcLikeFakeOutput && text.endsWith("\n")) { |
| // ServiceMessages protocol requires that every message |
| // should start with new line, so such behaviour may led to generating |
| // some number of useless \n. |
| // |
| // IDEA process handler flush output by size or line break |
| // So: |
| // 1. "a\n\nb\n" -> ["a\n", "\n", "b\n"] |
| // 2. "a\n##teamcity[..]\n" -> ["a\n", "#teamcity[..]\n"] |
| // We need distinguish 1) and 2) cases, in 2) first linebreak is redundant and must be ignored |
| // in 2) linebreak must be considered as output |
| // output will be in TestOutput message |
| // Lets set myPendingLineBreakFlag if we meet "\n" and then ignore it or apply depending on |
| // next output chunk |
| myPendingLineBreakFlag = true; |
| outputToProcess = outputToProcess.substring(0, outputToProcess.length() - 1); |
| } |
| //fire current output |
| fireOnUncapturedOutput(outputToProcess, outputType); |
| } |
| else { |
| myPendingLineBreakFlag = false; |
| } |
| } |
| catch (ParseException e) { |
| |
| LOG.error(getTFrameworkPrefix(myTestFrameworkName) + "Error parsing text: [" + text + "]", e); |
| } |
| } |
| |
| protected boolean processServiceMessages(final String text, |
| final Key outputType, |
| final ServiceMessageVisitor visitor) throws ParseException { |
| // service message parser expects line like "##teamcity[ .... ]" without whitespaces in the end. |
| final ServiceMessage message = ServiceMessage.parse(text.trim()); |
| if (message != null) { |
| message.visit(visitor); |
| } |
| return message != null; |
| } |
| |
| |
| private void fireOnTestStarted(@NotNull TestStartedEvent testStartedEvent) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestStarted(testStartedEvent); |
| } |
| } |
| |
| private void fireOnTestFailure(@NotNull TestFailedEvent testFailedEvent) { |
| assertNotNull(testFailedEvent.getLocalizedFailureMessage()); |
| |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestFailure(testFailedEvent); |
| } |
| } |
| |
| private void fireOnTestIgnored(@NotNull TestIgnoredEvent testIgnoredEvent) { |
| |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestIgnored(testIgnoredEvent); |
| } |
| } |
| |
| private void fireOnTestFinished(@NotNull TestFinishedEvent testFinishedEvent) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestFinished(testFinishedEvent); |
| } |
| } |
| |
| private void fireOnCustomProgressTestsCategory(final String categoryName, |
| int testsCount) { |
| assertNotNull(categoryName); |
| |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| final boolean disableCustomMode = StringUtil.isEmpty(categoryName); |
| processor.onCustomProgressTestsCategory(disableCustomMode ? null : categoryName, |
| disableCustomMode ? 0 : testsCount); |
| } |
| } |
| |
| private void fireOnCustomProgressTestStarted() { |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onCustomProgressTestStarted(); |
| } |
| } |
| |
| private void fireOnCustomProgressTestFailed() { |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onCustomProgressTestFailed(); |
| } |
| } |
| |
| private void fireOnTestFrameworkAttached() { |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestsReporterAttached(); |
| } |
| } |
| |
| private void fireOnTestOutput(@NotNull TestOutputEvent testOutputEvent) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestOutput(testOutputEvent); |
| } |
| } |
| |
| private void fireOnUncapturedOutput(final String text, final Key outputType) { |
| assertNotNull(text); |
| |
| if (StringUtil.isEmpty(text)) { |
| return; |
| } |
| |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onUncapturedOutput(text, outputType); |
| } |
| } |
| |
| private void fireOnTestsCountInSuite(final int count) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onTestsCountInSuite(count); |
| } |
| } |
| |
| private void fireOnSuiteStarted(@NotNull TestSuiteStartedEvent suiteStartedEvent) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onSuiteStarted(suiteStartedEvent); |
| } |
| } |
| |
| private void fireOnSuiteFinished(@NotNull TestSuiteFinishedEvent suiteFinishedEvent) { |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onSuiteFinished(suiteFinishedEvent); |
| } |
| } |
| |
| protected void fireOnErrorMsg(final String localizedMessage, |
| @Nullable final String stackTrace, |
| boolean isCritical) { |
| assertNotNull(localizedMessage); |
| |
| // local variable is used to prevent concurrent modification |
| final GeneralTestEventsProcessor processor = myProcessor; |
| if (processor != null) { |
| processor.onError(localizedMessage, stackTrace, isCritical); |
| } |
| } |
| |
| private void assertNotNull(final String s) { |
| if (s == null) { |
| LOG.error(getTFrameworkPrefix(myTestFrameworkName) + " @NotNull value is expected."); |
| } |
| } |
| |
| private class MyServiceMessageVisitor extends DefaultServiceMessageVisitor { |
| @NonNls public static final String KEY_TESTS_COUNT = "testCount"; |
| @NonNls private static final String ATTR_KEY_TEST_ERROR = "error"; |
| @NonNls private static final String ATTR_KEY_TEST_COUNT = "count"; |
| @NonNls private static final String ATTR_KEY_TEST_DURATION = "duration"; |
| @NonNls private static final String ATTR_KEY_LOCATION_URL = "locationHint"; |
| @NonNls private static final String ATTR_KEY_LOCATION_URL_OLD = "location"; |
| @NonNls private static final String ATTR_KEY_STACKTRACE_DETAILS = "details"; |
| @NonNls private static final String ATTR_KEY_DIAGNOSTIC = "diagnosticInfo"; |
| |
| @NonNls private static final String MESSAGE = "message"; |
| @NonNls private static final String TEST_REPORTER_ATTACHED = "enteredTheMatrix"; |
| @NonNls private static final String ATTR_KEY_STATUS = "status"; |
| @NonNls private static final String ATTR_VALUE_STATUS_ERROR = "ERROR"; |
| @NonNls private static final String ATTR_VALUE_STATUS_WARNING = "WARNING"; |
| @NonNls private static final String ATTR_KEY_TEXT = "text"; |
| @NonNls private static final String ATTR_KEY_ERROR_DETAILS = "errorDetails"; |
| |
| @NonNls public static final String CUSTOM_STATUS = "customProgressStatus"; |
| @NonNls private static final String ATTR_KEY_TEST_TYPE = "type"; |
| @NonNls private static final String ATTR_KEY_TESTS_CATEGORY = "testsCategory"; |
| @NonNls private static final String ATTR_VAL_TEST_STARTED = "testStarted"; |
| @NonNls private static final String ATTR_VAL_TEST_FAILED = "testFailed"; |
| |
| public void visitTestSuiteStarted(@NotNull final TestSuiteStarted suiteStarted) { |
| final String locationUrl = fetchTestLocation(suiteStarted); |
| TestSuiteStartedEvent suiteStartedEvent = new TestSuiteStartedEvent(suiteStarted, locationUrl); |
| fireOnSuiteStarted(suiteStartedEvent); |
| } |
| |
| @Nullable |
| private String fetchTestLocation(final TestSuiteStarted suiteStarted) { |
| final Map<String, String> attrs = suiteStarted.getAttributes(); |
| final String location = attrs.get(ATTR_KEY_LOCATION_URL); |
| if (location == null) { |
| // try old API |
| final String oldLocation = attrs.get(ATTR_KEY_LOCATION_URL_OLD); |
| if (oldLocation != null) { |
| LOG.error(getTFrameworkPrefix(myTestFrameworkName) |
| + |
| "Test Runner API was changed for TeamCity 5.0 compatibility. Please use 'locationHint' attribute instead of 'location'."); |
| return oldLocation; |
| } |
| return null; |
| } |
| return location; |
| } |
| |
| public void visitTestSuiteFinished(@NotNull final TestSuiteFinished suiteFinished) { |
| TestSuiteFinishedEvent finishedEvent = new TestSuiteFinishedEvent(suiteFinished); |
| fireOnSuiteFinished(finishedEvent); |
| } |
| |
| public void visitTestStarted(@NotNull final TestStarted testStarted) { |
| // TODO |
| // final String locationUrl = testStarted.getLocationHint(); |
| |
| final String locationUrl = testStarted.getAttributes().get(ATTR_KEY_LOCATION_URL); |
| TestStartedEvent testStartedEvent = new TestStartedEvent(testStarted, locationUrl); |
| fireOnTestStarted(testStartedEvent); |
| } |
| |
| public void visitTestFinished(@NotNull final TestFinished testFinished) { |
| //TODO |
| //final Integer duration = testFinished.getTestDuration(); |
| //fireOnTestFinished(testFinished.getTestName(), duration != null ? duration.intValue() : 0); |
| |
| final String durationStr = testFinished.getAttributes().get(ATTR_KEY_TEST_DURATION); |
| |
| // Test duration in milliseconds |
| long duration = 0; |
| |
| if (!StringUtil.isEmptyOrSpaces(durationStr)) { |
| duration = convertToLong(durationStr, testFinished); |
| } |
| |
| TestFinishedEvent testFinishedEvent = new TestFinishedEvent(testFinished, duration); |
| fireOnTestFinished(testFinishedEvent); |
| } |
| |
| public void visitTestIgnored(@NotNull final TestIgnored testIgnored) { |
| final String stacktrace = testIgnored.getAttributes().get(ATTR_KEY_STACKTRACE_DETAILS); |
| fireOnTestIgnored(new TestIgnoredEvent(testIgnored, stacktrace)); |
| } |
| |
| public void visitTestStdOut(@NotNull final TestStdOut testStdOut) { |
| fireOnTestOutput(new TestOutputEvent(testStdOut, testStdOut.getStdOut(), true)); |
| } |
| |
| public void visitTestStdErr(@NotNull final TestStdErr testStdErr) { |
| fireOnTestOutput(new TestOutputEvent(testStdErr, testStdErr.getStdErr(), false)); |
| } |
| |
| public void visitTestFailed(@NotNull final TestFailed testFailed) { |
| final boolean testError = testFailed.getAttributes().get(ATTR_KEY_TEST_ERROR) != null; |
| TestFailedEvent testFailedEvent = new TestFailedEvent(testFailed, testError); |
| fireOnTestFailure(testFailedEvent); |
| } |
| |
| public void visitPublishArtifacts(@NotNull final PublishArtifacts publishArtifacts) { |
| //Do nothing |
| } |
| |
| public void visitProgressMessage(@NotNull final ProgressMessage progressMessage) { |
| //Do nothing |
| } |
| |
| public void visitProgressStart(@NotNull final ProgressStart progressStart) { |
| //Do nothing |
| } |
| |
| public void visitProgressFinish(@NotNull final ProgressFinish progressFinish) { |
| //Do nothing |
| } |
| |
| public void visitBuildStatus(@NotNull final BuildStatus buildStatus) { |
| //Do nothing |
| } |
| |
| public void visitBuildNumber(@NotNull final BuildNumber buildNumber) { |
| //Do nothing |
| } |
| |
| public void visitBuildStatisticValue(@NotNull final BuildStatisticValue buildStatsValue) { |
| //Do nothing |
| } |
| |
| @Override |
| public void visitMessageWithStatus(@NotNull Message msg) { |
| final Map<String, String> msgAttrs = msg.getAttributes(); |
| |
| final String text = msgAttrs.get(ATTR_KEY_TEXT); |
| if (!StringUtil.isEmpty(text)) { |
| // msg status |
| final String status = msgAttrs.get(ATTR_KEY_STATUS); |
| if (status.equals(ATTR_VALUE_STATUS_ERROR)) { |
| // error msg |
| |
| final String stackTrace = msgAttrs.get(ATTR_KEY_ERROR_DETAILS); |
| fireOnErrorMsg(text, stackTrace, true); |
| } |
| else if (status.equals(ATTR_VALUE_STATUS_WARNING)) { |
| // warning msg |
| |
| // let's show warning via stderr |
| final String stackTrace = msgAttrs.get(ATTR_KEY_ERROR_DETAILS); |
| fireOnErrorMsg(text, stackTrace, false); |
| } |
| else { |
| // some other text |
| |
| // we cannot pass output type here but it is a service message |
| // let's think that is was stdout |
| fireOnUncapturedOutput(text, ProcessOutputTypes.STDOUT); |
| } |
| } |
| } |
| |
| public void visitServiceMessage(@NotNull final ServiceMessage msg) { |
| final String name = msg.getMessageName(); |
| |
| if (KEY_TESTS_COUNT.equals(name)) { |
| processTestCountInSuite(msg); |
| } |
| else if (CUSTOM_STATUS.equals(name)) { |
| processCustomStatus(msg); |
| } |
| else if (MESSAGE.equals(name)) { |
| final Map<String, String> msgAttrs = msg.getAttributes(); |
| |
| final String text = msgAttrs.get(ATTR_KEY_TEXT); |
| if (!StringUtil.isEmpty(text)) { |
| // some other text |
| |
| // we cannot pass output type here but it is a service message |
| // let's think that is was stdout |
| fireOnUncapturedOutput(text, ProcessOutputTypes.STDOUT); |
| } |
| } |
| else if (TEST_REPORTER_ATTACHED.equals(name)) { |
| fireOnTestFrameworkAttached(); |
| } |
| else { |
| GeneralToSMTRunnerEventsConvertor.logProblem(LOG, "Unexpected service message:" + name, myTestFrameworkName); |
| } |
| } |
| |
| private void processTestCountInSuite(final ServiceMessage msg) { |
| final String countStr = msg.getAttributes().get(ATTR_KEY_TEST_COUNT); |
| fireOnTestsCountInSuite(convertToInt(countStr, msg)); |
| } |
| |
| private int convertToInt(String countStr, final ServiceMessage msg) { |
| int count = 0; |
| try { |
| count = Integer.parseInt(countStr); |
| } |
| catch (NumberFormatException ex) { |
| final String diagnosticInfo = msg.getAttributes().get(ATTR_KEY_DIAGNOSTIC); |
| LOG.error(getTFrameworkPrefix(myTestFrameworkName) + "Parse integer error." + (diagnosticInfo == null ? "" : " " + diagnosticInfo), |
| ex); |
| } |
| return count; |
| } |
| |
| private long convertToLong(final String countStr, @NotNull final ServiceMessage msg) { |
| long count = 0; |
| try { |
| count = Long.parseLong(countStr); |
| } |
| catch (NumberFormatException ex) { |
| final String diagnosticInfo = msg.getAttributes().get(ATTR_KEY_DIAGNOSTIC); |
| LOG.error(getTFrameworkPrefix(myTestFrameworkName) + "Parse long error." + (diagnosticInfo == null ? "" : " " + diagnosticInfo), ex); |
| } |
| return count; |
| } |
| |
| private void processCustomStatus(final ServiceMessage msg) { |
| final Map<String, String> attrs = msg.getAttributes(); |
| final String msgType = attrs.get(ATTR_KEY_TEST_TYPE); |
| if (msgType != null) { |
| if (msgType.equals(ATTR_VAL_TEST_STARTED)) { |
| fireOnCustomProgressTestStarted(); |
| } |
| else if (msgType.equals(ATTR_VAL_TEST_FAILED)) { |
| fireOnCustomProgressTestFailed(); |
| } |
| return; |
| } |
| final String testsCategory = attrs.get(ATTR_KEY_TESTS_CATEGORY); |
| if (testsCategory != null) { |
| final String countStr = msg.getAttributes().get(ATTR_KEY_TEST_COUNT); |
| fireOnCustomProgressTestsCategory(testsCategory, convertToInt(countStr, msg)); |
| |
| //noinspection UnnecessaryReturnStatement |
| return; |
| } |
| } |
| } |
| } |