1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
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
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
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
301
302 testPlan.getTestIdentifier(
303 testIdentifier.getParentIdObject().get().toString());
304 return !parent.getParentIdObject().isPresent() ? testIdentifier : findTopParent(parent);
305 }
306
307
308
309
310
311
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
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
338
339 testPlan.getTestIdentifier(
340 testIdentifier.getParentIdObject().get().toString());
341 return findFirstParentContainerAndSourceClass(parent);
342 }
343
344
345
346
347
348
349
350
351
352
353
354
355 private ResultDisplay toClassMethodName(TestIdentifier testIdentifier) {
356
357
358
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
417
418
419
420
421
422
423
424
425
426
427
428
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);
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
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
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 }