junit 5 tags support (IDEA-163481)
[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.TestSource;
24 import org.junit.platform.engine.reporting.ReportEntry;
25 import org.junit.platform.engine.support.descriptor.ClassSource;
26 import org.junit.platform.engine.support.descriptor.CompositeTestSource;
27 import org.junit.platform.engine.support.descriptor.FileSource;
28 import org.junit.platform.engine.support.descriptor.MethodSource;
29 import org.junit.platform.launcher.TestExecutionListener;
30 import org.junit.platform.launcher.TestIdentifier;
31 import org.junit.platform.launcher.TestPlan;
32 import org.opentest4j.AssertionFailedError;
33 import org.opentest4j.MultipleFailuresError;
34 import org.opentest4j.ValueWrapper;
35
36 import java.io.File;
37 import java.io.PrintStream;
38 import java.io.PrintWriter;
39 import java.io.StringWriter;
40 import java.util.*;
41
42 public class JUnit5TestExecutionListener implements TestExecutionListener {
43   private static final String NO_LOCATION_HINT = "";
44   private static final String NO_LOCATION_HINT_VALUE = "";
45   private final PrintStream myPrintStream;
46   private TestPlan myTestPlan;
47   private long myCurrentTestStart;
48   private int myFinishCount = 0;
49   private String myRootName;
50   private boolean mySuccessful = true;
51   private String myIdSuffix = "";
52   private final Set<TestIdentifier> myActiveRoots = new HashSet<>();
53
54   public JUnit5TestExecutionListener() {
55     this(System.out);
56   }
57
58   public JUnit5TestExecutionListener(PrintStream printStream) {
59     myPrintStream = printStream;
60     myPrintStream.println("##teamcity[enteredTheMatrix]");
61   }
62
63   public boolean wasSuccessful() {
64     return mySuccessful;
65   }
66
67   public void initializeIdSuffix(boolean forked) {
68     if (forked && myIdSuffix.length() == 0) {
69       myIdSuffix = String.valueOf(System.currentTimeMillis());
70     }
71   }
72   
73   public void initializeIdSuffix(int i) {
74     myIdSuffix = i + "th"; 
75   }
76
77
78   @Override
79   public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
80     StringBuilder builder = new StringBuilder();
81     builder.append("timestamp = ").append(entry.getTimestamp());
82     entry.getKeyValuePairs().forEach((key, value) -> builder.append(", ").append(key).append(" = ").append(value));
83     myPrintStream.println(builder.toString());
84   }
85
86   @Override
87   public void testPlanExecutionStarted(TestPlan testPlan) {
88     if (myRootName != null) {
89       int lastPointIdx = myRootName.lastIndexOf('.');
90       String name = myRootName;
91       String comment = null;
92       if (lastPointIdx >= 0) {
93         name = myRootName.substring(lastPointIdx + 1);
94         comment = myRootName.substring(0, lastPointIdx);
95       }
96
97       myPrintStream.println("##teamcity[rootName name = \'" + escapeName(name) +
98                             (comment != null ? ("\' comment = \'" + escapeName(comment)) : "") + "\'" +
99                             " location = \'java:suite://" + escapeName(myRootName) +
100                             "\']");
101     }
102   }
103
104   @Override
105   public void testPlanExecutionFinished(TestPlan testPlan) {
106   }
107
108   @Override
109   public void executionSkipped(TestIdentifier testIdentifier, String reason) {
110     executionStarted (testIdentifier);
111     executionFinished(testIdentifier, TestExecutionResult.Status.ABORTED, null, reason);
112   }
113
114   @Override
115   public void executionStarted(TestIdentifier testIdentifier) {
116     if (testIdentifier.isTest()) {
117       testStarted(testIdentifier);
118       myCurrentTestStart = System.currentTimeMillis();
119     }
120     else if (hasNonTrivialParent(testIdentifier)) {
121       myFinishCount = 0;
122       myPrintStream.println("##teamcity[testSuiteStarted" + idAndName(testIdentifier) + getLocationHint(testIdentifier) + "]");
123     }
124   }
125
126   @Override
127   public void dynamicTestRegistered(TestIdentifier testIdentifier) {
128     myTestPlan.add(testIdentifier);
129   }
130
131   @Override
132   public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
133     final TestExecutionResult.Status status = testExecutionResult.getStatus();
134     final Throwable throwableOptional = testExecutionResult.getThrowable().orElse(null);
135     executionFinished(testIdentifier, status, throwableOptional, null);
136     mySuccessful &= TestExecutionResult.Status.SUCCESSFUL == testExecutionResult.getStatus();
137   }
138
139   private void executionFinished(TestIdentifier testIdentifier,
140                                  TestExecutionResult.Status status,
141                                  Throwable throwableOptional,
142                                  String reason) {
143     final String displayName = testIdentifier.getDisplayName();
144     if (testIdentifier.isTest()) {
145       final long duration = getDuration();
146       if (status == TestExecutionResult.Status.FAILED) {
147         testFailure(testIdentifier, MapSerializerUtil.TEST_FAILED, throwableOptional, duration, reason, true);
148       }
149       else if (status == TestExecutionResult.Status.ABORTED) {
150         testFailure(testIdentifier, MapSerializerUtil.TEST_IGNORED, throwableOptional, duration, reason, true);
151       }
152       testFinished(testIdentifier, duration);
153       myFinishCount++;
154     }
155     else if (hasNonTrivialParent(testIdentifier)){
156       String messageName = null;
157       if (status == TestExecutionResult.Status.FAILED) {
158         messageName = MapSerializerUtil.TEST_FAILED;
159       }
160       else if (status == TestExecutionResult.Status.ABORTED) {
161         messageName = MapSerializerUtil.TEST_IGNORED;
162       }
163       if (messageName != null) {
164         if (status == TestExecutionResult.Status.FAILED) {
165           String parentId = getParentId(testIdentifier);
166           String nameAndId = " name=\'" + JUnit4TestListener.CLASS_CONFIGURATION +
167                              "\' nodeId=\'" + escapeName(getId(testIdentifier)) +
168                              "\' parentNodeId=\'" + parentId + "\' ";
169           testFailure(JUnit4TestListener.CLASS_CONFIGURATION, getId(testIdentifier), parentId, messageName, throwableOptional, 0, reason, true);
170           myPrintStream.println("\n##teamcity[testFinished" + nameAndId + "]");
171         }
172
173         final Set<TestIdentifier> descendants = myTestPlan != null ? myTestPlan.getDescendants(testIdentifier) : Collections.emptySet();
174         if (!descendants.isEmpty() && myFinishCount == 0) {
175           for (TestIdentifier childIdentifier : descendants) {
176             testStarted(childIdentifier);
177             testFailure(childIdentifier, MapSerializerUtil.TEST_IGNORED, status == TestExecutionResult.Status.ABORTED ? throwableOptional : null, 0, reason, status == TestExecutionResult.Status.ABORTED);
178             testFinished(childIdentifier, 0);
179           }
180           myFinishCount = 0;
181         }
182       }
183       myPrintStream.println("##teamcity[testSuiteFinished " + idAndName(testIdentifier, displayName) + "]");
184     }
185   }
186
187   private boolean hasNonTrivialParent(TestIdentifier testIdentifier) {
188     return testIdentifier.getParentId().isPresent() || (myActiveRoots.size() > 1 && myActiveRoots.contains(testIdentifier));
189   }
190
191   protected long getDuration() {
192     return System.currentTimeMillis() - myCurrentTestStart;
193   }
194
195   private void testStarted(TestIdentifier testIdentifier) {
196     myPrintStream.println("\n##teamcity[testStarted" + idAndName(testIdentifier) + " " + getLocationHint(testIdentifier) + "]");
197   }
198   
199   private void testFinished(TestIdentifier testIdentifier, long duration) {
200     myPrintStream.println("\n##teamcity[testFinished" + idAndName(testIdentifier) + (duration > 0 ? " duration=\'" + Long.toString(duration) + "\'" : "") + "]");
201   }
202
203   private void testFailure(TestIdentifier testIdentifier,
204                            String messageName,
205                            Throwable ex,
206                            long duration,
207                            String reason,
208                            boolean includeThrowable) {
209     testFailure(testIdentifier.getDisplayName(), getId(testIdentifier), getParentId(testIdentifier), messageName, ex, duration, reason, includeThrowable);
210   }
211
212   private void testFailure(String methodName,
213                            String id,
214                            String parentId,
215                            String messageName,
216                            Throwable ex,
217                            long duration,
218                            String reason,
219                            boolean includeThrowable) {
220     final Map<String, String> attrs = new LinkedHashMap<>();
221     attrs.put("name", methodName);
222     attrs.put("id", id);
223     attrs.put("nodeId", id);
224     attrs.put("parentNodeId", parentId);
225     if (duration > 0) {
226       attrs.put("duration", Long.toString(duration));
227     }
228     if (reason != null) {
229       attrs.put("message", reason);
230     }
231     try {
232       if (ex != null) {
233         ComparisonFailureData failureData = null;
234         if (ex instanceof MultipleFailuresError && ((MultipleFailuresError)ex).hasFailures()) {
235           for (Throwable assertionError : ((MultipleFailuresError)ex).getFailures()) {
236             testFailure(methodName, id, parentId, messageName, assertionError, duration, reason, false);
237           }
238         }
239         else if (ex instanceof AssertionFailedError && ((AssertionFailedError)ex).isActualDefined() && ((AssertionFailedError)ex).isExpectedDefined()) {
240           final ValueWrapper actual = ((AssertionFailedError)ex).getActual();
241           final ValueWrapper expected = ((AssertionFailedError)ex).getExpected();
242           failureData = new ComparisonFailureData(expected.getStringRepresentation(), actual.getStringRepresentation());
243         }
244         else {
245           //try to detect failure with junit 4 if present in the classpath
246           try {
247             failureData = ExpectedPatterns.createExceptionNotification(ex);
248           }
249           catch (Throwable ignore) {}
250         }
251
252         if (includeThrowable || failureData == null) {
253           ComparisonFailureData.registerSMAttributes(failureData, getTrace(ex), ex.getMessage(), attrs, ex, "Comparison Failure: ", "expected: <");
254         }
255         else {
256           ComparisonFailureData.registerSMAttributes(failureData, "", "", attrs, ex, "", "expected: <");
257         }
258       }
259     }
260     finally {
261       myPrintStream.println("\n" + MapSerializerUtil.asString(messageName, attrs));
262     }
263   }
264
265   protected String getTrace(Throwable ex) {
266     final StringWriter stringWriter = new StringWriter();
267     final PrintWriter writer = new PrintWriter(stringWriter);
268     ex.printStackTrace(writer);
269     return stringWriter.toString();
270   }
271
272   public void setTestPlan(TestPlan testPlan) {
273     myTestPlan = testPlan;
274   }
275
276   public void sendTree(TestPlan testPlan, String rootName) {
277     myTestPlan = testPlan;
278     myRootName = rootName;
279     if (Boolean.parseBoolean(System.getProperty("idea.junit.show.engines", "true"))) {
280       testPlan.getRoots().stream().filter(root1 -> !testPlan.getChildren(root1).isEmpty()).forEach(myActiveRoots::add);
281     }
282     if (myActiveRoots.size() > 1) {
283       for (TestIdentifier root : myActiveRoots) {
284         sendTreeUnderRoot(testPlan, root, new HashSet<>());
285       }
286     }
287     else { //skip engine node when one engine available
288       for (TestIdentifier root : testPlan.getRoots()) {
289         assert root.isContainer();
290         for (TestIdentifier testIdentifier : testPlan.getChildren(root)) {
291           sendTreeUnderRoot(testPlan, testIdentifier, new HashSet<>());
292         }
293       }
294     }
295     myPrintStream.println("##teamcity[treeEnded]");
296   }
297
298   private String getId(TestIdentifier identifier) {
299     return identifier.getUniqueId() + myIdSuffix;
300   }
301
302   private void sendTreeUnderRoot(TestPlan testPlan,
303                                  TestIdentifier root,
304                                  HashSet<TestIdentifier> visited) {
305     final String idAndName = idAndName(root);
306     if (root.isContainer()) {
307       myPrintStream.println("##teamcity[suiteTreeStarted" + idAndName + " " + getLocationHint(root) + "]");
308       for (TestIdentifier childIdentifier : testPlan.getChildren(root)) {
309         if (visited.add(childIdentifier)) {
310           sendTreeUnderRoot(testPlan, childIdentifier, visited);
311         }
312         else {
313           System.err.println("Identifier \'" + getId(childIdentifier) + "\' is reused");
314         }
315       }
316       myPrintStream.println("##teamcity[suiteTreeEnded" + idAndName + "]");
317     }
318     else if (root.isTest()) {
319       myPrintStream.println("##teamcity[suiteTreeNode " + idAndName + " " + getLocationHint(root) + "]");
320     }
321   }
322
323   private String idAndName(TestIdentifier testIdentifier) {
324     return idAndName(testIdentifier, testIdentifier.getDisplayName());
325   }
326
327   private String idAndName(TestIdentifier testIdentifier, String displayName) {
328     return " id=\'" + escapeName(getId(testIdentifier)) +
329            "\' name=\'" + escapeName(displayName) +
330            "\' nodeId=\'" + escapeName(getId(testIdentifier)) +
331            "\' parentNodeId=\'" + escapeName(getParentId(testIdentifier)) + "\'";
332   }
333
334   private String getParentId(TestIdentifier testIdentifier) {
335     Optional<TestIdentifier> parent = myTestPlan.getParent(testIdentifier);
336     if (myActiveRoots.size() <= 1 && !parent.map(identifier -> identifier.getParentId().orElse(null)).isPresent()) {
337       return "0";
338     }
339
340     return parent
341       .map(identifier -> identifier.getUniqueId() + myIdSuffix)
342       .orElse("0");
343   }
344
345   static String getLocationHint(TestIdentifier root) {
346     return root.getSource()
347       .map(testSource -> getLocationHintValue(testSource))
348       .filter(maybeLocationHintValue -> !NO_LOCATION_HINT_VALUE.equals(maybeLocationHintValue))
349       .map(locationHintValue -> "locationHint=\'" + locationHintValue + "\'" + getMetainfo(root))
350       .orElse(NO_LOCATION_HINT);
351   }
352
353   private static String getMetainfo(TestIdentifier root) {
354     return root.getSource()
355       .filter(testSource -> testSource instanceof MethodSource)
356       .map(testSource -> " metainfo=\'" + ((MethodSource)testSource).getMethodParameterTypes() + "\'")
357       .orElse(NO_LOCATION_HINT);
358   }
359   
360   static String getLocationHintValue(TestSource testSource) {
361
362     if (testSource instanceof CompositeTestSource) {
363       CompositeTestSource compositeTestSource = ((CompositeTestSource)testSource);
364       for (TestSource sourceFromComposite : compositeTestSource.getSources()) {
365         String locationHintValue = getLocationHintValue(sourceFromComposite);
366         if (!NO_LOCATION_HINT_VALUE.equals(locationHintValue)) {
367           return locationHintValue;
368         }
369       }
370       return NO_LOCATION_HINT_VALUE;
371     }
372
373     if (testSource instanceof FileSource) {
374       FileSource fileSource = (FileSource)testSource;
375       File file = fileSource.getFile();
376       String line = fileSource.getPosition()
377         .map(position -> ":" + position.getLine())
378         .orElse("");
379       return "file://" + file.getAbsolutePath() + line;
380     }
381
382     if (testSource instanceof MethodSource) {
383       MethodSource methodSource = (MethodSource)testSource;
384       return javaLocation(methodSource.getClassName(), methodSource.getMethodName(), true);
385     }
386
387     if (testSource instanceof ClassSource) {
388       String className = ((ClassSource)testSource).getClassName();
389       return javaLocation(className, null, false);
390     }
391
392     return NO_LOCATION_HINT_VALUE;
393   }
394
395   private static String javaLocation(String className, String maybeMethodName, boolean isTest) {
396     String type = isTest ? "test" : "suite";
397     String methodName = maybeMethodName == null ? "" : "." + maybeMethodName;
398     String location = escapeName(className + methodName);
399     return "java:" + type + "://" + location;
400   }
401
402   private static String escapeName(String str) {
403     return MapSerializerUtil.escapeStr(str, MapSerializerUtil.STD_ESCAPER);
404   }
405
406   static String getClassName(TestIdentifier description) {
407     return description.getSource().map(source -> {
408       if (source instanceof MethodSource) {
409         return ((MethodSource)source).getClassName();
410       }
411       if (source instanceof ClassSource) {
412         return ((ClassSource)source).getClassName();
413       }
414       return null;
415     }).orElse(null);
416   }
417
418   static String getMethodName(TestIdentifier testIdentifier) {
419     return testIdentifier.getSource().map((source) -> {
420       if (source instanceof MethodSource) {
421         return ((MethodSource)source).getMethodName();
422       }
423       return null;
424     }).orElse(null);
425   }
426   
427   static String getMethodSignature(TestIdentifier testIdentifier) {
428     return testIdentifier.getSource().map((source) -> {
429       if (source instanceof MethodSource) {
430         String parameterTypes = ((MethodSource)source).getMethodParameterTypes();
431         return ((MethodSource)source).getMethodName() + (parameterTypes != null ? "(" + parameterTypes + ")" : "");
432       }
433       return null;
434     }).orElse(null);
435   }
436 }