junit 5: don't miss 'after' container failures (IDEA-164028)
[idea/community.git] / plugins / junit5_rt / src / com / intellij / junit5 / JUnit5TestExecutionListener.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.junit5;
17
18 import com.intellij.junit4.ExpectedPatterns;
19 import com.intellij.junit4.JUnit4TestListener;
20 import com.intellij.rt.execution.junit.ComparisonFailureData;
21 import com.intellij.rt.execution.junit.MapSerializerUtil;
22 import org.junit.platform.engine.TestExecutionResult;
23 import org.junit.platform.engine.reporting.ReportEntry;
24 import org.junit.platform.engine.support.descriptor.JavaClassSource;
25 import org.junit.platform.engine.support.descriptor.JavaMethodSource;
26 import org.junit.platform.launcher.TestExecutionListener;
27 import org.junit.platform.launcher.TestIdentifier;
28 import org.junit.platform.launcher.TestPlan;
29 import org.opentest4j.AssertionFailedError;
30 import org.opentest4j.MultipleFailuresError;
31 import org.opentest4j.ValueWrapper;
32
33 import java.io.PrintStream;
34 import java.io.PrintWriter;
35 import java.io.StringWriter;
36 import java.util.HashMap;
37 import java.util.Map;
38 import java.util.Set;
39
40 public class JUnit5TestExecutionListener implements TestExecutionListener {
41   private final PrintStream myPrintStream;
42   private TestPlan myTestPlan;
43   private long myCurrentTestStart;
44   private int myFinishCount;
45   private String myRootName;
46   private Set<TestIdentifier> myRoots;
47   private boolean mySuccessful;
48
49   public JUnit5TestExecutionListener() {
50     this(System.out);
51   }
52
53   public JUnit5TestExecutionListener(PrintStream printStream) {
54     myPrintStream = printStream;
55     myPrintStream.println("##teamcity[enteredTheMatrix]");
56   }
57
58   public boolean wasSuccessful() {
59     return mySuccessful;
60   }
61
62   public void initialize() {
63     mySuccessful = true;
64     myFinishCount = 0;
65   }
66
67   @Override
68   public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
69     StringBuilder builder = new StringBuilder();
70     builder.append("timestamp = ").append(entry.getTimestamp());
71     entry.getKeyValuePairs().forEach((key, value) -> builder.append(", ").append(key).append(" = ").append(value));
72     myPrintStream.println(builder.toString());
73   }
74
75   @Override
76   public void testPlanExecutionStarted(TestPlan testPlan) {
77     if (myRootName != null) {
78       int lastPointIdx = myRootName.lastIndexOf('.');
79       String name = myRootName;
80       String comment = null;
81       if (lastPointIdx >= 0) {
82         name = myRootName.substring(lastPointIdx + 1);
83         comment = myRootName.substring(0, lastPointIdx);
84       }
85
86       myPrintStream.println("##teamcity[rootName name = \'" + escapeName(name) +
87                             (comment != null ? ("\' comment = \'" + escapeName(comment)) : "") + "\'" +
88                             " location = \'java:suite://" + escapeName(myRootName) +
89                             "\']");
90     }
91   }
92
93   @Override
94   public void testPlanExecutionFinished(TestPlan testPlan) {
95     myTestPlan = null;
96   }
97
98   @Override
99   public void executionSkipped(TestIdentifier testIdentifier, String reason) {
100     executionStarted (testIdentifier);
101     executionFinished(testIdentifier, TestExecutionResult.Status.ABORTED, null, reason);
102   }
103
104   @Override
105   public void executionStarted(TestIdentifier testIdentifier) {
106     if (testIdentifier.isTest()) {
107       testStarted(testIdentifier);
108       myCurrentTestStart = System.currentTimeMillis();
109     }
110     else if (!myRoots.contains(testIdentifier)){
111       myFinishCount = 0;
112       myPrintStream.println("##teamcity[testSuiteStarted" + idAndName(testIdentifier) + "\']");
113     }
114   }
115
116   private static String idAndName(TestIdentifier testIdentifier) {
117     return idAndName(testIdentifier, testIdentifier.getDisplayName());
118   }
119
120   private static String idAndName(TestIdentifier testIdentifier, String displayName) {
121     return " id=\'" + testIdentifier.getUniqueId().toString() + "\' name=\'" + escapeName(displayName);
122   }
123
124   @Override
125   public void dynamicTestRegistered(TestIdentifier testIdentifier) {
126     int i = 0;
127   }
128
129   @Override
130   public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
131     final TestExecutionResult.Status status = testExecutionResult.getStatus();
132     final Throwable throwableOptional = testExecutionResult.getThrowable().orElse(null);
133     executionFinished(testIdentifier, status, throwableOptional, null);
134     mySuccessful &= TestExecutionResult.Status.SUCCESSFUL == testExecutionResult.getStatus();
135   }
136
137   private void executionFinished(TestIdentifier testIdentifier,
138                                  TestExecutionResult.Status status,
139                                  Throwable throwableOptional,
140                                  String reason) {
141     final String displayName = testIdentifier.getDisplayName();
142     if (testIdentifier.isTest()) {
143       final long duration = getDuration();
144       if (status == TestExecutionResult.Status.FAILED) {
145         testFailure(testIdentifier, MapSerializerUtil.TEST_FAILED, throwableOptional, duration, reason, true);
146       }
147       else if (status == TestExecutionResult.Status.ABORTED) {
148         testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, duration, reason, true);
149       }
150       testFinished(testIdentifier, duration);
151       myFinishCount++;
152     }
153     else if (!myRoots.contains(testIdentifier)){
154       String messageName = null;
155       if (status == TestExecutionResult.Status.FAILED) {
156         messageName = MapSerializerUtil.TEST_FAILED;
157       }
158       else if (status == TestExecutionResult.Status.ABORTED) {
159         messageName = MapSerializerUtil.TEST_IGNORED;
160       }
161       if (messageName != null) {
162         if (status == TestExecutionResult.Status.FAILED) {
163           myPrintStream.println("\n##teamcity[testStarted name=\'" + JUnit4TestListener.CLASS_CONFIGURATION + "\']");
164           testFailure(JUnit4TestListener.CLASS_CONFIGURATION, JUnit4TestListener.CLASS_CONFIGURATION, messageName, throwableOptional, 0, reason, true);
165           myPrintStream.println("\n##teamcity[testFinished name=\'" + JUnit4TestListener.CLASS_CONFIGURATION + "\']");
166         }
167
168         final Set<TestIdentifier> descendants = myTestPlan.getDescendants(testIdentifier);
169         if (!descendants.isEmpty() && myFinishCount == 0) {
170           for (TestIdentifier childIdentifier : descendants) {
171             testStarted(childIdentifier);
172             testFailure(childIdentifier, MapSerializerUtil.TEST_IGNORED, status == TestExecutionResult.Status.ABORTED ? throwableOptional : null, 0, reason, status == TestExecutionResult.Status.ABORTED);
173             testFinished(childIdentifier, 0);
174           }
175         }
176       }
177       myPrintStream.println("##teamcity[testSuiteFinished " + idAndName(testIdentifier, displayName) + "\']");
178     }
179   }
180
181   protected long getDuration() {
182     return System.currentTimeMillis() - myCurrentTestStart;
183   }
184
185   private void testStarted(TestIdentifier testIdentifier) {
186     myPrintStream.println("\n##teamcity[testStarted" + idAndName(testIdentifier) + getLocationHint(testIdentifier) + "\']");
187   }
188   
189   private void testFinished(TestIdentifier testIdentifier, long duration) {
190     myPrintStream.println("\n##teamcity[testFinished" + idAndName(testIdentifier) + (duration > 0 ? "\' duration=\'" + Long.toString(duration) : "") + "\']");
191   }
192
193   private void testFailure(TestIdentifier testIdentifier,
194                            String messageName,
195                            Throwable ex,
196                            long duration,
197                            String reason,
198                            boolean includeThrowable) {
199     testFailure(testIdentifier.getDisplayName(), testIdentifier.getUniqueId().toString(), messageName, ex, duration, reason, includeThrowable);
200   }
201
202   private void testFailure(String methodName,
203                            String id,
204                            String messageName,
205                            Throwable ex,
206                            long duration,
207                            String reason,
208                            boolean includeThrowable) {
209     final Map<String, String> attrs = new HashMap<>();
210     attrs.put("name", methodName);
211     attrs.put("id", id);
212     if (duration > 0) {
213       attrs.put("duration", Long.toString(duration));
214     }
215     if (reason != null) {
216       attrs.put("message", reason);
217     }
218     try {
219       if (ex != null) {
220         ComparisonFailureData failureData = null;
221         if (ex instanceof MultipleFailuresError && ((MultipleFailuresError)ex).hasFailures()) {
222           for (AssertionError assertionError : ((MultipleFailuresError)ex).getFailures()) {
223             testFailure(methodName, id, messageName, assertionError, duration, reason, false);
224           }
225         }
226         else if (ex instanceof AssertionFailedError && ((AssertionFailedError)ex).isActualDefined() && ((AssertionFailedError)ex).isExpectedDefined()) {
227           final ValueWrapper actual = ((AssertionFailedError)ex).getActual();
228           final ValueWrapper expected = ((AssertionFailedError)ex).getExpected();
229           failureData = new ComparisonFailureData(expected.getStringRepresentation(), actual.getStringRepresentation());
230         }
231         else {
232           //try to detect failure with junit 4 if present in the classpath
233           try {
234             failureData = ExpectedPatterns.createExceptionNotification(ex);
235           }
236           catch (Throwable ignore) {}
237         }
238
239         if (includeThrowable || failureData == null) {
240           ComparisonFailureData.registerSMAttributes(failureData, getTrace(ex), ex.getMessage(), attrs, ex);
241         }
242         else {
243           ComparisonFailureData.registerSMAttributes(failureData, "", "", attrs, ex, "");
244         }
245       }
246     }
247     finally {
248       myPrintStream.println("\n" + MapSerializerUtil.asString(messageName, attrs));
249     }
250   }
251
252   protected String getTrace(Throwable ex) {
253     final StringWriter stringWriter = new StringWriter();
254     final PrintWriter writer = new PrintWriter(stringWriter);
255     ex.printStackTrace(writer);
256     return stringWriter.toString();
257   }
258
259
260   public void sendTree(TestPlan testPlan, String rootName) {
261     myTestPlan = testPlan;
262     myRootName = rootName;
263     myRoots = testPlan.getRoots();
264     for (TestIdentifier root : myRoots) {
265       assert root.isContainer();
266       for (TestIdentifier testIdentifier : testPlan.getChildren(root)) {
267         sendTreeUnderRoot(testPlan, testIdentifier);
268       }
269     }
270     myPrintStream.println("##teamcity[treeEnded]");
271   }
272
273   private void sendTreeUnderRoot(TestPlan testPlan, TestIdentifier root) {
274     final String idAndName = idAndName(root);
275     if (root.isContainer()) {
276       myPrintStream.println("##teamcity[suiteTreeStarted" + idAndName + getLocationHint(root) + "\']");
277       for (TestIdentifier childIdentifier : testPlan.getChildren(root)) {
278         sendTreeUnderRoot(testPlan, childIdentifier);
279       }
280       myPrintStream.println("##teamcity[suiteTreeEnded" + idAndName + "\']");
281     }
282     else if (root.isTest()) {
283       myPrintStream.println("##teamcity[suiteTreeNode " + idAndName + getLocationHint(root) + "\']");
284     }
285   }
286
287   private String getLocationHint(TestIdentifier root) {
288     final String className = getClassName(root);
289     final String methodName = getMethodName(root);
290     return "\' locationHint=\'java:" + (root.isTest() ? "test" : "suite") + "://" + escapeName(className + (methodName != null ? "." + methodName : ""));
291   }
292
293
294   private static String escapeName(String str) {
295     return MapSerializerUtil.escapeStr(str, MapSerializerUtil.STD_ESCAPER);
296   }
297
298   static String getClassName(TestIdentifier description) {
299     return description.getSource().map(source -> {
300       if (source instanceof JavaMethodSource) {
301         return ((JavaMethodSource)source).getJavaClass().getName();
302       }
303       if (source instanceof JavaClassSource) {
304         return ((JavaClassSource)source).getJavaClass().getName();
305       }
306       return null;
307     }).orElse(null);
308   }
309
310   static String getMethodName(TestIdentifier testIdentifier) {
311     return testIdentifier.getSource().map((source) -> {
312       if (source instanceof JavaMethodSource) {
313         return ((JavaMethodSource)source).getJavaMethodName();
314       }
315       return null;
316     }).orElse(null);
317   }
318 }