2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com
.intellij
.execution
.testframework
.sm
.runner
;
18 import com
.intellij
.execution
.process
.ProcessOutputTypes
;
19 import com
.intellij
.openapi
.diagnostic
.Logger
;
20 import com
.intellij
.openapi
.util
.Key
;
21 import com
.intellij
.openapi
.util
.text
.StringUtil
;
22 import jetbrains
.buildServer
.messages
.serviceMessages
.*;
23 import org
.jetbrains
.annotations
.NonNls
;
24 import org
.jetbrains
.annotations
.NotNull
;
25 import org
.jetbrains
.annotations
.Nullable
;
27 import java
.text
.ParseException
;
28 import java
.util
.ArrayList
;
29 import java
.util
.List
;
33 * @author Roman Chernyatchik
35 * This implementation also supports messages splitted in parts by early flush.
36 * Implementation assumes that buffer is being flushed on line end or by timer,
37 * i.e. incomming text contains no more than one line's end marker ('\r', '\n', or "\r\n")
38 * (e.g. process was run with IDEA program's runner)
40 public class OutputToGeneralTestEventsConverter
implements ProcessOutputConsumer
{
41 private static final Logger LOG
= Logger
.getInstance(OutputToGeneralTestEventsConverter
.class.getName());
43 private GeneralTestEventsProcessor myProcessor
;
44 private final MyServiceMessageVisitor myServiceMessageVisitor
;
46 private static class OutputChunk
{
48 private String myText
;
50 private OutputChunk(Key key
, String text
) {
59 public String
getText() {
63 public void append(String text
) {
68 private final List
<OutputChunk
> myOutputChunks
;
70 public OutputToGeneralTestEventsConverter() {
71 myServiceMessageVisitor
= new MyServiceMessageVisitor();
72 myOutputChunks
= new ArrayList
<OutputChunk
>();
75 public void setProcessor(final GeneralTestEventsProcessor processor
) {
76 myProcessor
= processor
;
79 public void process(final String text
, final Key outputType
) {
80 if (outputType
!= ProcessOutputTypes
.STDERR
&& outputType
!= ProcessOutputTypes
.SYSTEM
) {
81 // we check for consistensy only std output
82 // because all events must be send to stdout
83 processStdOutConsistently(text
, outputType
);
85 processConsistentText(text
, outputType
, false);
90 * Flashes the rest of stdout text buffer after output has been stopped
92 public void flushBufferBeforeTerminating() {
93 flushStdOutputBuffer();
96 public void dispose() {
100 private void flushStdOutputBuffer() {
101 // if osColoredProcessHandler was attached it can split string with several colors
102 // in several parts. Thus '\n' symbol may be send as one part with some color
103 // such situation should differ from single '\n' from process that is used by TC reporters
104 // to separate TC commands from other stuff + optimize flushing
105 // TODO: probably in IDEA mode such runners shouldn't add explicit \n because we can
106 // successfully process broken messages across several flushes
107 // size of parts may tell us either \n was single in original flushed data or it was
108 // separated by process handler
109 List
<OutputChunk
> chunks
= new ArrayList
<OutputChunk
>();
110 OutputChunk lastChunk
= null;
111 synchronized (myOutputChunks
) {
112 for (OutputChunk chunk
: myOutputChunks
) {
113 if (lastChunk
!= null && chunk
.getKey() == lastChunk
.getKey()) {
114 lastChunk
.append(chunk
.getText());
122 myOutputChunks
.clear();
124 final boolean isTCLikeFakeOutput
= chunks
.size() == 1;
125 for (OutputChunk chunk
: chunks
) {
126 processConsistentText(chunk
.getText(), chunk
.getKey(), isTCLikeFakeOutput
);
130 private void processStdOutConsistently(final String text
, final Key outputType
) {
131 final int textLength
= text
.length();
132 if (textLength
== 0) {
136 synchronized (myOutputChunks
) {
137 myOutputChunks
.add(new OutputChunk(outputType
, text
));
140 final char lastChar
= text
.charAt(textLength
- 1);
141 if (lastChar
== '\n' || lastChar
== '\r') {
142 // buffer contains consistent string
143 flushStdOutputBuffer();
147 private void processConsistentText(final String text
, final Key outputType
, boolean tcLikeFakeOutput
) {
149 final ServiceMessage serviceMessage
= ServiceMessage
.parse(text
);
150 if (serviceMessage
!= null) {
151 serviceMessage
.visit(myServiceMessageVisitor
);
154 if (text
.equals("\n") && tcLikeFakeOutput
) {
155 // ServiceMessages protocol requires that every message
156 // should start with new line, so such behaviour may led to generating
157 // some number of useless \n.
159 // This will not affect tests output because all
160 // output will be in TestOutput message
163 //fire current output
164 fireOnUncapturedOutput(text
, outputType
);
167 catch (ParseException e
) {
172 private void fireOnTestStarted(final String testName
, @Nullable final String locationUrl
) {
173 // local variable is used to prevent concurrent modification
174 final GeneralTestEventsProcessor processor
= myProcessor
;
175 if (processor
!= null) {
176 processor
.onTestStarted(testName
, locationUrl
);
180 private void fireOnTestFailure(final String testName
, final String localizedMessage
, final String stackTrace
,
181 final boolean isTestError
) {
183 // local variable is used to prevent concurrent modification
184 final GeneralTestEventsProcessor processor
= myProcessor
;
185 if (processor
!= null) {
186 processor
.onTestFailure(testName
, localizedMessage
, stackTrace
, isTestError
);
190 private void fireOnTestIgnored(final String testName
, final String ignoreComment
,
191 @Nullable final String details
) {
193 // local variable is used to prevent concurrent modification
194 final GeneralTestEventsProcessor processor
= myProcessor
;
195 if (processor
!= null) {
196 processor
.onTestIgnored(testName
, ignoreComment
, details
);
200 private void fireOnTestFinished(final String testName
, final int duration
) {
201 // local variable is used to prevent concurrent modification
202 final GeneralTestEventsProcessor processor
= myProcessor
;
203 if (processor
!= null) {
204 processor
.onTestFinished(testName
, duration
);
208 private void fireOnCustomProgressTestsCategory(@NotNull final String categoryName
, int testsCount
) {
209 final GeneralTestEventsProcessor processor
= myProcessor
;
210 if (processor
!= null) {
211 final boolean disableCustomMode
= StringUtil
.isEmpty(categoryName
);
212 processor
.onCustomProgressTestsCategory(disableCustomMode ?
null : categoryName
,
213 disableCustomMode ?
0 : testsCount
);
217 private void fireOnCustomProgressTestStarted() {
218 final GeneralTestEventsProcessor processor
= myProcessor
;
219 if (processor
!= null) {
220 processor
.onCustomProgressTestStarted();
224 private void fireOnCustomProgressTestFailed() {
225 final GeneralTestEventsProcessor processor
= myProcessor
;
226 if (processor
!= null) {
227 processor
.onCustomProgressTestFailed();
231 private void fireOnTestOutput(final String testName
, final String text
, final boolean stdOut
) {
232 // local variable is used to prevent concurrent modification
233 final GeneralTestEventsProcessor processor
= myProcessor
;
234 if (processor
!= null) {
235 processor
.onTestOutput(testName
, text
, stdOut
);
239 private void fireOnUncapturedOutput(final String text
, final Key outputType
) {
240 // local variable is used to prevent concurrent modification
241 final GeneralTestEventsProcessor processor
= myProcessor
;
242 if (processor
!= null) {
243 processor
.onUncapturedOutput(text
, outputType
);
247 private void fireOnTestsCountInSuite(final int count
) {
248 // local variable is used to prevent concurrent modification
249 final GeneralTestEventsProcessor processor
= myProcessor
;
250 if (processor
!= null) {
251 processor
.onTestsCountInSuite(count
);
255 private void fireOnSuiteStarted(final String suiteName
, @Nullable final String locationUrl
) {
256 // local variable is used to prevent concurrent modification
257 final GeneralTestEventsProcessor processor
= myProcessor
;
258 if (processor
!= null) {
259 processor
.onSuiteStarted(suiteName
, locationUrl
);
263 private void fireOnSuiteFinished(final String suiteName
) {
264 // local variable is used to prevent concurrent modification
265 final GeneralTestEventsProcessor processor
= myProcessor
;
266 if (processor
!= null) {
267 processor
.onSuiteFinished(suiteName
);
271 private class MyServiceMessageVisitor
extends DefaultServiceMessageVisitor
{
272 @NonNls public static final String KEY_TESTS_COUNT
= "testCount";
273 @NonNls private static final String ATTR_KEY_TEST_ERROR
= "error";
274 @NonNls private static final String ATTR_KEY_TEST_COUNT
= "count";
275 @NonNls private static final String ATTR_KEY_TEST_DURATION
= "duration";
276 @NonNls private static final String ATTR_KEY_LOCATION_URL
= "locationHint";
277 @NonNls private static final String ATTR_KEY_LOCATION_URL_OLD
= "location";
278 @NonNls private static final String ATTR_KEY_STACKTRACE_DETAILS
= "details";
280 @NonNls public static final String CUSTOM_STATUS
= "customProgressStatus";
281 @NonNls private static final String ATTR_KEY_TEST_TYPE
= "type";
282 @NonNls private static final String ATTR_KEY_TESTS_CATEGORY
= "testsCategory";
283 @NonNls private static final String ATTR_VAL_TEST_STARTED
= "testStarted";
284 @NonNls private static final String ATTR_VAL_TEST_FAILED
= "testFailed";
286 public void visitTestSuiteStarted(@NotNull final TestSuiteStarted suiteStarted
) {
287 final String locationUrl
= fetchTestLocation(suiteStarted
);
288 fireOnSuiteStarted(suiteStarted
.getSuiteName(), locationUrl
);
292 private String
fetchTestLocation(TestSuiteStarted suiteStarted
) {
293 final Map
<String
, String
> attrs
= suiteStarted
.getAttributes();
294 final String location
= attrs
.get(ATTR_KEY_LOCATION_URL
);
295 if (location
== null) {
297 final String oldLocation
= attrs
.get(ATTR_KEY_LOCATION_URL_OLD
);
298 if (oldLocation
!= null) {
299 LOG
.error("Test Runner API was changed for TeamCity 5.0 compatibility. Please use 'locationHint' attribute instead of 'location'.");
307 public void visitTestSuiteFinished(@NotNull final TestSuiteFinished suiteFinished
) {
308 fireOnSuiteFinished(suiteFinished
.getSuiteName());
311 public void visitTestStarted(@NotNull final TestStarted testStarted
) {
312 final String locationUrl
= testStarted
.getAttributes().get(ATTR_KEY_LOCATION_URL
);
313 fireOnTestStarted(testStarted
.getTestName(), locationUrl
);
316 public void visitTestFinished(@NotNull final TestFinished testFinished
) {
318 //final Integer duration = testFinished.getTestDuration();
319 //fireOnTestFinished(testFinished.getTestName(), duration != null ? duration.intValue() : 0);
321 final String durationStr
= testFinished
.getAttributes().get(ATTR_KEY_TEST_DURATION
);
323 // Test's duration in milliseconds
326 if (!StringUtil
.isEmptyOrSpaces(durationStr
)) {
328 duration
= Integer
.parseInt(durationStr
);
329 } catch (NumberFormatException ex
) {
334 fireOnTestFinished(testFinished
.getTestName(), duration
);
337 public void visitTestIgnored(@NotNull final TestIgnored testIgnored
) {
338 final String details
= testIgnored
.getAttributes().get(ATTR_KEY_STACKTRACE_DETAILS
);
339 fireOnTestIgnored(testIgnored
.getTestName(), testIgnored
.getIgnoreComment(), details
);
342 public void visitTestStdOut(@NotNull final TestStdOut testStdOut
) {
343 fireOnTestOutput(testStdOut
.getTestName(),testStdOut
.getStdOut(), true);
346 public void visitTestStdErr(@NotNull final TestStdErr testStdErr
) {
347 fireOnTestOutput(testStdErr
.getTestName(),testStdErr
.getStdErr(), false);
350 public void visitTestFailed(@NotNull final TestFailed testFailed
) {
351 final boolean isTestError
= testFailed
.getAttributes().get(ATTR_KEY_TEST_ERROR
) != null;
353 fireOnTestFailure(testFailed
.getTestName(), testFailed
.getFailureMessage(), testFailed
.getStacktrace(), isTestError
);
356 public void visitPublishArtifacts(@NotNull final PublishArtifacts publishArtifacts
) {
360 public void visitProgressMessage(@NotNull final ProgressMessage progressMessage
) {
364 public void visitProgressStart(@NotNull final ProgressStart progressStart
) {
368 public void visitProgressFinish(@NotNull final ProgressFinish progressFinish
) {
372 public void visitBuildStatus(@NotNull final BuildStatus buildStatus
) {
376 public void visitBuildNumber(@NotNull final BuildNumber buildNumber
) {
380 public void visitBuildStatisticValue(@NotNull final BuildStatisticValue buildStatsValue
) {
384 public void visitServiceMessage(@NotNull final ServiceMessage msg
) {
385 final String name
= msg
.getMessageName();
387 if (KEY_TESTS_COUNT
.equals(name
)) {
388 processTestCountInSuite(msg
);
389 } else if (CUSTOM_STATUS
.equals(name
)) {
390 processCustomStatus(msg
);
396 private void processTestCountInSuite(final ServiceMessage msg
) {
397 final String countStr
= msg
.getAttributes().get(ATTR_KEY_TEST_COUNT
);
398 fireOnTestsCountInSuite(convertToInt(countStr
));
401 private int convertToInt(String countStr
) {
404 count
= Integer
.parseInt(countStr
);
405 } catch (NumberFormatException ex
) {
411 private void processCustomStatus(final ServiceMessage msg
) {
412 final Map
<String
,String
> attrs
= msg
.getAttributes();
413 final String msgType
= attrs
.get(ATTR_KEY_TEST_TYPE
);
414 if (msgType
!= null) {
415 if (msgType
.equals(ATTR_VAL_TEST_STARTED
)) {
416 fireOnCustomProgressTestStarted();
417 } else if (msgType
.equals(ATTR_VAL_TEST_FAILED
)) {
418 fireOnCustomProgressTestFailed();
422 final String testsCategory
= attrs
.get(ATTR_KEY_TESTS_CATEGORY
);
423 if (testsCategory
!= null) {
424 final String countStr
= msg
.getAttributes().get(ATTR_KEY_TEST_COUNT
);
425 fireOnCustomProgressTestsCategory(testsCategory
, convertToInt(countStr
));
427 //noinspection UnnecessaryReturnStatement