View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.surefire.junitplatform;
20  
21  import java.lang.annotation.Annotation;
22  import java.lang.reflect.Method;
23  import java.util.Map;
24  import java.util.Objects;
25  import java.util.Optional;
26  import java.util.concurrent.ConcurrentHashMap;
27  import java.util.concurrent.ConcurrentMap;
28  import java.util.stream.Stream;
29  
30  import org.apache.maven.surefire.api.report.OutputReportEntry;
31  import org.apache.maven.surefire.api.report.RunMode;
32  import org.apache.maven.surefire.api.report.SafeThrowable;
33  import org.apache.maven.surefire.api.report.SimpleReportEntry;
34  import org.apache.maven.surefire.api.report.StackTraceWriter;
35  import org.apache.maven.surefire.api.report.Stoppable;
36  import org.apache.maven.surefire.api.report.TestOutputReceiver;
37  import org.apache.maven.surefire.api.report.TestOutputReportEntry;
38  import org.apache.maven.surefire.api.report.TestReportListener;
39  import org.apache.maven.surefire.api.util.ReflectionUtils;
40  import org.apache.maven.surefire.report.ClassMethodIndexer;
41  import org.apache.maven.surefire.report.PojoStackTraceWriter;
42  import org.apache.maven.surefire.report.RunModeSetter;
43  import org.junit.platform.engine.TestExecutionResult;
44  import org.junit.platform.engine.TestSource;
45  import org.junit.platform.engine.UniqueId;
46  import org.junit.platform.engine.support.descriptor.ClassSource;
47  import org.junit.platform.engine.support.descriptor.MethodSource;
48  import org.junit.platform.launcher.TestExecutionListener;
49  import org.junit.platform.launcher.TestIdentifier;
50  import org.junit.platform.launcher.TestPlan;
51  
52  import static java.util.Collections.emptyMap;
53  import static java.util.stream.Collectors.joining;
54  import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps;
55  import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
56  import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
57  
58  /**
59   * @since 2.22.0
60   */
61  final class RunListenerAdapter implements TestExecutionListener, TestOutputReceiver<OutputReportEntry>, RunModeSetter {
62      private final ClassMethodIndexer classMethodIndexer = new ClassMethodIndexer();
63      private final ConcurrentMap<TestIdentifier, Long> testStartTime = new ConcurrentHashMap<>();
64      private final ConcurrentMap<TestIdentifier, TestExecutionResult> failures = new ConcurrentHashMap<>();
65      private final ConcurrentMap<String, TestIdentifier> runningTestIdentifiersByUniqueId = new ConcurrentHashMap<>();
66      private final TestReportListener<TestOutputReportEntry> runListener;
67      private final Stoppable stoppable;
68      private volatile TestPlan testPlan;
69      private volatile RunMode runMode;
70  
71      RunListenerAdapter(TestReportListener<TestOutputReportEntry> runListener, Stoppable stoppable) {
72          this.runListener = runListener;
73          this.stoppable = stoppable;
74      }
75  
76      @Override
77      public void setRunMode(RunMode runMode) {
78          this.runMode = runMode;
79      }
80  
81      @Override
82      public void testPlanExecutionStarted(TestPlan testPlan) {
83          this.testPlan = testPlan;
84      }
85  
86      @Override
87      public void testPlanExecutionFinished(TestPlan testPlan) {
88          this.testPlan = null;
89          testStartTime.clear();
90      }
91  
92      @Override
93      public void executionStarted(TestIdentifier testIdentifier) {
94          runningTestIdentifiersByUniqueId.put(testIdentifier.getUniqueId(), testIdentifier);
95  
96          if (testIdentifier.isContainer()
97                  && testIdentifier
98                          .getSource()
99                          .filter(ClassSource.class::isInstance)
100                         .isPresent()) {
101             testStartTime.put(testIdentifier, System.currentTimeMillis());
102             runListener.testSetStarting(createReportEntry(testIdentifier));
103         } else if (testIdentifier.isTest()) {
104             testStartTime.put(testIdentifier, System.currentTimeMillis());
105             runListener.testStarting(createReportEntry(testIdentifier));
106         }
107     }
108 
109     @Override
110     public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
111         boolean isClass = testIdentifier.isContainer()
112                 && testIdentifier
113                         .getSource()
114                         .filter(ClassSource.class::isInstance)
115                         .isPresent();
116 
117         boolean isTest = testIdentifier.isTest();
118 
119         boolean failed = testExecutionResult.getStatus() == FAILED;
120 
121         boolean isAssertionError = testExecutionResult
122                 .getThrowable()
123                 .filter(AssertionError.class::isInstance)
124                 .isPresent();
125 
126         boolean isRootContainer =
127                 testIdentifier.isContainer() && !testIdentifier.getParentId().isPresent();
128 
129         if (failed || isClass || isTest) {
130             Integer elapsed = computeElapsedTime(testIdentifier);
131             switch (testExecutionResult.getStatus()) {
132                 case ABORTED:
133                     if (isTest) {
134                         runListener.testAssumptionFailure(
135                                 createReportEntry(testIdentifier, testExecutionResult, elapsed));
136                     } else {
137                         runListener.testSetCompleted(
138                                 createReportEntry(testIdentifier, testExecutionResult, systemProps(), null, elapsed));
139                     }
140                     break;
141                 case FAILED:
142                     String reason =
143                             safeGetMessage(testExecutionResult.getThrowable().orElse(null));
144                     SimpleReportEntry reportEntry =
145                             createReportEntry(testIdentifier, testExecutionResult, reason, elapsed);
146                     if (isAssertionError) {
147                         runListener.testFailed(reportEntry);
148                     } else {
149                         runListener.testError(reportEntry);
150                     }
151                     if (isClass || isRootContainer) {
152                         runListener.testSetCompleted(
153                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
154                     }
155                     failures.put(testIdentifier, testExecutionResult);
156                     fireStopEvent();
157                     break;
158                 default:
159                     if (isTest) {
160                         runListener.testSucceeded(createReportEntry(testIdentifier, null, elapsed));
161                     } else {
162                         runListener.testSetCompleted(
163                                 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
164                     }
165             }
166         }
167 
168         runningTestIdentifiersByUniqueId.remove(testIdentifier.getUniqueId());
169     }
170 
171     private Integer computeElapsedTime(TestIdentifier testIdentifier) {
172         Long startTime = testStartTime.remove(testIdentifier);
173         long endTime = System.currentTimeMillis();
174         return startTime == null ? null : (int) (endTime - startTime);
175     }
176 
177     private Stream<TestIdentifier> collectAllTestIdentifiersInHierarchy(TestIdentifier testIdentifier) {
178         return testIdentifier
179                 .getParentId()
180                 .map(runningTestIdentifiersByUniqueId::get)
181                 .map(parentTestIdentifier -> Stream.concat(
182                         Stream.of(parentTestIdentifier), collectAllTestIdentifiersInHierarchy(parentTestIdentifier)))
183                 .orElseGet(Stream::empty);
184     }
185 
186     private String safeGetMessage(Throwable throwable) {
187         try {
188             SafeThrowable t = throwable == null ? null : new SafeThrowable(throwable);
189             return t == null ? null : t.getMessage();
190         } catch (Throwable t) {
191             return t.getMessage();
192         }
193     }
194 
195     @Override
196     public void executionSkipped(TestIdentifier testIdentifier, String reason) {
197         boolean isClass = testIdentifier.isContainer()
198                 && testIdentifier
199                         .getSource()
200                         .filter(ClassSource.class::isInstance)
201                         .isPresent();
202 
203         testStartTime.remove(testIdentifier);
204 
205         if (isClass) {
206             SimpleReportEntry report = createReportEntry(testIdentifier);
207             runListener.testSetStarting(report);
208             for (TestIdentifier child : testPlan.getChildren(testIdentifier)) {
209                 runListener.testSkipped(createReportEntry(child, null, emptyMap(), reason, null));
210             }
211             runListener.testSetCompleted(report);
212         } else {
213             runListener.testSkipped(createReportEntry(testIdentifier, null, emptyMap(), reason, null));
214         }
215     }
216 
217     private SimpleReportEntry createReportEntry(
218             TestIdentifier testIdentifier,
219             TestExecutionResult testExecutionResult,
220             Map<String, String> systemProperties,
221             String reason,
222             Integer elapsedTime) {
223         ResultDisplay classMethodName = toClassMethodName(testIdentifier);
224 
225         String className = classMethodName.getClassName();
226 
227         String classText = classMethodName.getDisplayName();
228         if (Objects.equals(className, classText)) {
229             classText = null;
230         }
231         if (classMethodName.getClassDisplayName() != null) {
232             classText = classMethodName.getClassDisplayName();
233         }
234 
235         // classText = classMethodName[0]; // testIdentifier.getLegacyReportingName();
236 
237         boolean failed = testExecutionResult == null || testExecutionResult.getStatus() == FAILED;
238         String methodName = failed || testIdentifier.isTest() ? classMethodName.getMethodSignature() : null;
239         String methodText = failed || testIdentifier.isTest() ? classMethodName.getMethodDisplayName() : null;
240         if (Objects.equals(methodName, methodText)) {
241             methodText = null;
242         }
243         StackTraceWriter stw =
244                 testExecutionResult == null ? null : toStackTraceWriter(className, methodName, testExecutionResult);
245         return new SimpleReportEntry(
246                 runMode,
247                 classMethodIndexer.indexClassMethod(className, methodName),
248                 className,
249                 classText,
250                 methodName,
251                 methodText,
252                 stw,
253                 elapsedTime,
254                 reason,
255                 systemProperties);
256     }
257 
258     private SimpleReportEntry createReportEntry(TestIdentifier testIdentifier) {
259         return createReportEntry(testIdentifier, null, null);
260     }
261 
262     private SimpleReportEntry createReportEntry(
263             TestIdentifier testIdentifier, TestExecutionResult testExecutionResult, Integer elapsedTime) {
264         return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), null, elapsedTime);
265     }
266 
267     private SimpleReportEntry createReportEntry(
268             TestIdentifier testIdentifier,
269             TestExecutionResult testExecutionResult,
270             String reason,
271             Integer elapsedTime) {
272         return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), reason, elapsedTime);
273     }
274 
275     private StackTraceWriter toStackTraceWriter(
276             String realClassName, String realMethodName, TestExecutionResult testExecutionResult) {
277         switch (testExecutionResult.getStatus()) {
278             case ABORTED:
279             case FAILED:
280                 // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
281                 Throwable exception = testExecutionResult.getThrowable().orElse(null);
282                 return toStackTraceWriter(realClassName, realMethodName, exception);
283             default:
284                 return testExecutionResult
285                         .getThrowable()
286                         .map(t -> toStackTraceWriter(realClassName, realMethodName, t))
287                         .orElse(null);
288         }
289     }
290 
291     private StackTraceWriter toStackTraceWriter(String realClassName, String realMethodName, Throwable throwable) {
292         return new PojoStackTraceWriter(realClassName, realMethodName, throwable);
293     }
294 
295     private TestIdentifier findTopParent(TestIdentifier testIdentifier) {
296         if (!hasParentId(testIdentifier)) {
297             return testIdentifier;
298         }
299         TestIdentifier parent =
300                 // Get the parent test identifier using the parent ID object is from 1.10
301                 // use deprecated method
302                 testPlan.getTestIdentifier(
303                         testIdentifier.getParentIdObject().get().toString());
304         return !parent.getParentIdObject().isPresent() ? testIdentifier : findTopParent(parent);
305     }
306 
307     /**
308      * Checks if the test identifier has a parent ID but using reflection as it's only available from 1.8
309      *
310      * @param testIdentifier the test identifier to check
311      * @return true if the test identifier has a parent ID, false otherwise
312      */
313     private boolean hasParentId(TestIdentifier testIdentifier) {
314         Method getParentIdObjectMethod = ReflectionUtils.tryGetMethod(testIdentifier.getClass(), "getParentIdObject");
315         if (getParentIdObjectMethod == null) {
316             return false;
317         }
318         try {
319             Optional<UniqueId> uniqueIdOptional = (Optional<UniqueId>) getParentIdObjectMethod.invoke(testIdentifier);
320             return uniqueIdOptional.isPresent();
321         } catch (Throwable ignore) {
322             // ignore this
323         }
324         return false;
325     }
326 
327     private TestIdentifier findFirstParentContainerAndSourceClass(TestIdentifier testIdentifier) {
328         if (!hasParentId(testIdentifier)
329                 || (testIdentifier.isContainer()
330                         && testIdentifier
331                                 .getSource()
332                                 .filter(ClassSource.class::isInstance)
333                                 .isPresent())) {
334             return testIdentifier;
335         }
336         TestIdentifier parent =
337                 // Get the parent test identifier using the parent ID object is from 1.10
338                 // use deprecated method
339                 testPlan.getTestIdentifier(
340                         testIdentifier.getParentIdObject().get().toString());
341         return findFirstParentContainerAndSourceClass(parent);
342     }
343 
344     /**
345      * <ul>
346      *     <li>[0] class name - used in stacktrace parser</li>
347      *     <li>[1] class display name</li>
348      *     <li>[2] method signature - used in stacktrace parser</li>
349      *     <li>[3] method display name</li>
350      * </ul>
351      *
352      * @param testIdentifier a class or method
353      * @return 4 elements string array
354      */
355     private ResultDisplay toClassMethodName(TestIdentifier testIdentifier) {
356 
357         // find the first class or method source in the hierarchy just below the root level
358         // without parent and with ClassSource
359         Optional<String> classLevelName = Optional.empty();
360         TestIdentifier parent = findTopParent(testIdentifier);
361         if (parent != null
362                 && parent.getSource().filter(ClassSource.class::isInstance).isPresent()) {
363             ClassSource classSource = (ClassSource) parent.getSource().get();
364             classLevelName = Optional.of(classSource.getClassName());
365         }
366 
367         String classDisplayName = null;
368 
369         if (testIdentifier.isTest()) {
370             TestIdentifier parentContainer = findFirstParentContainerAndSourceClass(testIdentifier);
371             Optional<String> parentDisplayNameTagValue = findDisplayNameTagValue(parentContainer);
372             classDisplayName = parentDisplayNameTagValue.orElse(parentContainer.getLegacyReportingName());
373         }
374         Optional<TestSource> testSource = testIdentifier.getSource();
375         String display = testIdentifier.getDisplayName();
376 
377         if (testSource.filter(MethodSource.class::isInstance).isPresent()) {
378             MethodSource methodSource = testSource.map(MethodSource.class::cast).get();
379             String realClassName = methodSource.getClassName();
380             ResultDisplay source = collectAllTestIdentifiersInHierarchy(testIdentifier)
381                     .filter(i ->
382                             i.getSource().map(ClassSource.class::isInstance).orElse(false))
383                     .findFirst()
384                     .map(this::toClassMethodName)
385                     .map(s -> new ResultDisplay(
386                             s.getClassName(), s.getDisplayName(), null, null, s.getClassDisplayName()))
387                     .orElse(new ResultDisplay(realClassName, realClassName, null, null, classDisplayName));
388 
389             String parentDisplay = collectAllTestIdentifiersInHierarchy(testIdentifier)
390                     .filter(identifier -> identifier
391                             .getSource()
392                             .filter(MethodSource.class::isInstance)
393                             .isPresent())
394                     .map(TestIdentifier::getDisplayName)
395                     .collect(joining(" "));
396 
397             boolean needsSpaceSeparator = isNotBlank(parentDisplay) && !display.startsWith("[");
398             String methodDisplay = parentDisplay + (needsSpaceSeparator ? " " : "") + display;
399 
400             boolean isParameterized = isNotBlank(methodSource.getMethodParameterTypes());
401             boolean hasParameterizedParent = collectAllTestIdentifiersInHierarchy(testIdentifier)
402                     .filter(identifier -> !identifier.getSource().isPresent())
403                     .map(TestIdentifier::getLegacyReportingName)
404                     .anyMatch(legacyReportingName -> legacyReportingName.matches("^\\[.+]$"));
405             boolean isTestTemplate = testIdentifier.getLegacyReportingName().matches("^.*\\[\\d+]$");
406 
407             boolean parameterized = isParameterized || hasParameterizedParent || isTestTemplate;
408             String methodName = methodSource.getMethodName();
409             String description = testIdentifier.getLegacyReportingName();
410             boolean equalDescriptions = methodDisplay.equals(description);
411             boolean hasLegacyDescription = description.startsWith(methodName + '(');
412             boolean hasDisplayName = !equalDescriptions || !hasLegacyDescription;
413             String methodDesc = parameterized ? description : methodName;
414             String methodDisp = hasDisplayName ? methodDisplay : methodDesc;
415 
416             // The behavior of methods getLegacyReportingName() and getDisplayName().
417             //     junit4    ||  legacy  |  display
418             // ==============||==========|==========
419             //     normal    ||     m    |     m
420             //     param     ||   m[0]   |   m[0]
421             //  param+displ  || m[displ] | m[displ]
422 
423             //     junit5    ||  legacy  |  display
424             // ==============||==========|==========
425             //    normal     ||    m()   |    m()
426             //  normal+displ ||    m()   |   displ
427             //     param     ||  m()[1]  | [1] <param>
428             //  param+displ  ||  m()[1]  |   displ
429 
430             return new ResultDisplay(
431                     classLevelName.orElse(source.getClassName()),
432                     source.getDisplayName(),
433                     methodDesc,
434                     methodDisp,
435                     classDisplayName);
436         } else if (testSource.filter(ClassSource.class::isInstance).isPresent()) {
437             ClassSource classSource = testSource.map(ClassSource.class::cast).get();
438             String className = classSource.getClassName();
439             Optional<String> displayNameTagValue = findDisplayNameTagValue(testIdentifier);
440             return new ResultDisplay(
441                     classLevelName.orElse(className),
442                     displayNameTagValue.orElse(className),
443                     null,
444                     null,
445                     classDisplayName); // != null ? classDisplayName : className);
446         } else {
447             String source = testPlan.getParent(testIdentifier)
448                     .map(TestIdentifier::getDisplayName)
449                     .orElse(display);
450             return new ResultDisplay(classLevelName.orElse(source), source, display, display, classDisplayName);
451         }
452     }
453 
454     private Optional<String> findDisplayNameTagValue(TestIdentifier testIdentifier) {
455         try {
456             Class<? extends Annotation> displayNameClazz = (Class<? extends Annotation>)
457                     Thread.currentThread().getContextClassLoader().loadClass("org.junit.jupiter.api.DisplayName");
458             Optional<?> displayNameAnn = testIdentifier
459                     .getSource()
460                     .filter(ClassSource.class::isInstance)
461                     .map(s -> ((ClassSource) s).getJavaClass())
462                     .filter(m -> m.isAnnotationPresent(displayNameClazz))
463                     .map(method -> method.getAnnotation(displayNameClazz));
464 
465             Method valueMethod = displayNameClazz.getMethod("value");
466             return displayNameAnn.map(a -> {
467                 try {
468                     return (String) valueMethod.invoke(a);
469                 } catch (Throwable e) {
470                     return null;
471                 }
472             });
473         } catch (Exception e) {
474             // very old version of JUnit 5, may not have DisplayName annotation
475             return Optional.empty();
476         }
477     }
478 
479     private static class ResultDisplay {
480         private String className, displayName, methodSignature, methodDisplayName, classDisplayName;
481 
482         ResultDisplay(
483                 String className,
484                 String displayName,
485                 String methodSignature,
486                 String methodDisplayName,
487                 String classDisplayName) {
488             this.className = className;
489             this.displayName = displayName;
490             this.methodSignature = methodSignature;
491             this.methodDisplayName = methodDisplayName;
492             this.classDisplayName = classDisplayName;
493         }
494 
495         public String getClassName() {
496             return className;
497         }
498 
499         public String getDisplayName() {
500             return displayName;
501         }
502 
503         public String getMethodSignature() {
504             return methodSignature;
505         }
506 
507         public String getMethodDisplayName() {
508             return methodDisplayName;
509         }
510 
511         public String getClassDisplayName() {
512             return classDisplayName;
513         }
514     }
515 
516     /**
517      * @return Map of tests that failed.
518      */
519     Map<TestIdentifier, TestExecutionResult> getFailures() {
520         return failures;
521     }
522 
523     boolean hasFailingTests() {
524         return !getFailures().isEmpty();
525     }
526 
527     void reset() {
528         getFailures().clear();
529         testPlan = null;
530     }
531 
532     @Override
533     public void writeTestOutput(OutputReportEntry reportEntry) {
534         Long testRunId = classMethodIndexer.getLocalIndex();
535         runListener.writeTestOutput(new TestOutputReportEntry(reportEntry, runMode, testRunId));
536     }
537 
538     private void fireStopEvent() {
539         if (runMode == RunMode.NORMAL_RUN) {
540             stoppable.fireStopEvent();
541         }
542     }
543 }