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.util.Collections;
22 import java.util.List;
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.TestOutputReceiver;
36 import org.apache.maven.surefire.api.report.TestOutputReportEntry;
37 import org.apache.maven.surefire.api.report.TestReportListener;
38 import org.apache.maven.surefire.report.ClassMethodIndexer;
39 import org.apache.maven.surefire.report.PojoStackTraceWriter;
40 import org.apache.maven.surefire.report.RunModeSetter;
41 import org.junit.platform.engine.TestExecutionResult;
42 import org.junit.platform.engine.TestSource;
43 import org.junit.platform.engine.support.descriptor.ClassSource;
44 import org.junit.platform.engine.support.descriptor.MethodSource;
45 import org.junit.platform.launcher.TestExecutionListener;
46 import org.junit.platform.launcher.TestIdentifier;
47 import org.junit.platform.launcher.TestPlan;
48
49 import static java.util.Collections.emptyMap;
50 import static java.util.stream.Collectors.joining;
51 import static java.util.stream.Collectors.toList;
52 import static org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps;
53 import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
54 import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
55
56
57
58
59 final class RunListenerAdapter implements TestExecutionListener, TestOutputReceiver<OutputReportEntry>, RunModeSetter {
60 private final ClassMethodIndexer classMethodIndexer = new ClassMethodIndexer();
61 private final ConcurrentMap<TestIdentifier, Long> testStartTime = new ConcurrentHashMap<>();
62 private final ConcurrentMap<TestIdentifier, TestExecutionResult> failures = new ConcurrentHashMap<>();
63 private final ConcurrentMap<String, TestIdentifier> runningTestIdentifiersByUniqueId = new ConcurrentHashMap<>();
64 private final TestReportListener<TestOutputReportEntry> runListener;
65 private volatile TestPlan testPlan;
66 private volatile RunMode runMode;
67
68 RunListenerAdapter(TestReportListener<TestOutputReportEntry> runListener) {
69 this.runListener = runListener;
70 }
71
72 @Override
73 public void setRunMode(RunMode runMode) {
74 this.runMode = runMode;
75 }
76
77 @Override
78 public void testPlanExecutionStarted(TestPlan testPlan) {
79 this.testPlan = testPlan;
80 }
81
82 @Override
83 public void testPlanExecutionFinished(TestPlan testPlan) {
84 this.testPlan = null;
85 testStartTime.clear();
86 }
87
88 @Override
89 public void executionStarted(TestIdentifier testIdentifier) {
90 runningTestIdentifiersByUniqueId.put(testIdentifier.getUniqueId(), testIdentifier);
91
92 if (testIdentifier.isContainer()
93 && testIdentifier
94 .getSource()
95 .filter(ClassSource.class::isInstance)
96 .isPresent()) {
97 testStartTime.put(testIdentifier, System.currentTimeMillis());
98 runListener.testSetStarting(createReportEntry(testIdentifier));
99 } else if (testIdentifier.isTest()) {
100 testStartTime.put(testIdentifier, System.currentTimeMillis());
101 runListener.testStarting(createReportEntry(testIdentifier));
102 }
103 }
104
105 @Override
106 public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
107 boolean isClass = testIdentifier.isContainer()
108 && testIdentifier
109 .getSource()
110 .filter(ClassSource.class::isInstance)
111 .isPresent();
112
113 boolean isTest = testIdentifier.isTest();
114
115 boolean failed = testExecutionResult.getStatus() == FAILED;
116
117 boolean isAssertionError = testExecutionResult
118 .getThrowable()
119 .filter(AssertionError.class::isInstance)
120 .isPresent();
121
122 boolean isRootContainer =
123 testIdentifier.isContainer() && !testIdentifier.getParentId().isPresent();
124
125 if (failed || isClass || isTest) {
126 Integer elapsed = computeElapsedTime(testIdentifier);
127 switch (testExecutionResult.getStatus()) {
128 case ABORTED:
129 if (isTest) {
130 runListener.testAssumptionFailure(
131 createReportEntry(testIdentifier, testExecutionResult, elapsed));
132 } else {
133 runListener.testSetCompleted(
134 createReportEntry(testIdentifier, testExecutionResult, systemProps(), null, elapsed));
135 }
136 break;
137 case FAILED:
138 String reason =
139 safeGetMessage(testExecutionResult.getThrowable().orElse(null));
140 SimpleReportEntry reportEntry =
141 createReportEntry(testIdentifier, testExecutionResult, reason, elapsed);
142 if (isAssertionError) {
143 runListener.testFailed(reportEntry);
144 } else {
145 runListener.testError(reportEntry);
146 }
147 if (isClass || isRootContainer) {
148 runListener.testSetCompleted(
149 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
150 }
151 failures.put(testIdentifier, testExecutionResult);
152 break;
153 default:
154 if (isTest) {
155 runListener.testSucceeded(createReportEntry(testIdentifier, null, elapsed));
156 } else {
157 runListener.testSetCompleted(
158 createReportEntry(testIdentifier, null, systemProps(), null, elapsed));
159 }
160 }
161 }
162
163 runningTestIdentifiersByUniqueId.remove(testIdentifier.getUniqueId());
164 }
165
166 private Integer computeElapsedTime(TestIdentifier testIdentifier) {
167 Long startTime = testStartTime.remove(testIdentifier);
168 long endTime = System.currentTimeMillis();
169 return startTime == null ? null : (int) (endTime - startTime);
170 }
171
172 private Stream<TestIdentifier> collectAllTestIdentifiersInHierarchy(TestIdentifier testIdentifier) {
173 return testIdentifier
174 .getParentId()
175 .map(runningTestIdentifiersByUniqueId::get)
176 .map(parentTestIdentifier -> Stream.concat(
177 Stream.of(parentTestIdentifier), collectAllTestIdentifiersInHierarchy(parentTestIdentifier)))
178 .orElseGet(Stream::empty);
179 }
180
181 private String safeGetMessage(Throwable throwable) {
182 try {
183 SafeThrowable t = throwable == null ? null : new SafeThrowable(throwable);
184 return t == null ? null : t.getMessage();
185 } catch (Throwable t) {
186 return t.getMessage();
187 }
188 }
189
190 @Override
191 public void executionSkipped(TestIdentifier testIdentifier, String reason) {
192 boolean isClass = testIdentifier.isContainer()
193 && testIdentifier
194 .getSource()
195 .filter(ClassSource.class::isInstance)
196 .isPresent();
197
198 testStartTime.remove(testIdentifier);
199
200 if (isClass) {
201 SimpleReportEntry report = createReportEntry(testIdentifier);
202 runListener.testSetStarting(report);
203 for (TestIdentifier child : testPlan.getChildren(testIdentifier)) {
204 runListener.testSkipped(createReportEntry(child, null, emptyMap(), reason, null));
205 }
206 runListener.testSetCompleted(report);
207 } else {
208 runListener.testSkipped(createReportEntry(testIdentifier, null, emptyMap(), reason, null));
209 }
210 }
211
212 private SimpleReportEntry createReportEntry(
213 TestIdentifier testIdentifier,
214 TestExecutionResult testExecutionResult,
215 Map<String, String> systemProperties,
216 String reason,
217 Integer elapsedTime) {
218 String[] classMethodName = toClassMethodName(testIdentifier);
219 String className = classMethodName[0];
220 String classText = classMethodName[1];
221 if (Objects.equals(className, classText)) {
222 classText = null;
223 }
224 boolean failed = testExecutionResult == null || testExecutionResult.getStatus() == FAILED;
225 String methodName = failed || testIdentifier.isTest() ? classMethodName[2] : null;
226 String methodText = failed || testIdentifier.isTest() ? classMethodName[3] : null;
227 if (Objects.equals(methodName, methodText)) {
228 methodText = null;
229 }
230 StackTraceWriter stw =
231 testExecutionResult == null ? null : toStackTraceWriter(className, methodName, testExecutionResult);
232 return new SimpleReportEntry(
233 runMode,
234 classMethodIndexer.indexClassMethod(className, methodName),
235 className,
236 classText,
237 methodName,
238 methodText,
239 stw,
240 elapsedTime,
241 reason,
242 systemProperties);
243 }
244
245 private SimpleReportEntry createReportEntry(TestIdentifier testIdentifier) {
246 return createReportEntry(testIdentifier, null, null);
247 }
248
249 private SimpleReportEntry createReportEntry(
250 TestIdentifier testIdentifier, TestExecutionResult testExecutionResult, Integer elapsedTime) {
251 return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), null, elapsedTime);
252 }
253
254 private SimpleReportEntry createReportEntry(
255 TestIdentifier testIdentifier,
256 TestExecutionResult testExecutionResult,
257 String reason,
258 Integer elapsedTime) {
259 return createReportEntry(testIdentifier, testExecutionResult, emptyMap(), reason, elapsedTime);
260 }
261
262 private StackTraceWriter toStackTraceWriter(
263 String realClassName, String realMethodName, TestExecutionResult testExecutionResult) {
264 switch (testExecutionResult.getStatus()) {
265 case ABORTED:
266 case FAILED:
267
268 Throwable exception = testExecutionResult.getThrowable().orElse(null);
269 return toStackTraceWriter(realClassName, realMethodName, exception);
270 default:
271 return testExecutionResult
272 .getThrowable()
273 .map(t -> toStackTraceWriter(realClassName, realMethodName, t))
274 .orElse(null);
275 }
276 }
277
278 private StackTraceWriter toStackTraceWriter(String realClassName, String realMethodName, Throwable throwable) {
279 return new PojoStackTraceWriter(realClassName, realMethodName, throwable);
280 }
281
282
283
284
285
286
287
288
289
290
291
292
293 private String[] toClassMethodName(TestIdentifier testIdentifier) {
294 Optional<TestSource> testSource = testIdentifier.getSource();
295 String display = testIdentifier.getDisplayName();
296
297 if (testSource.filter(MethodSource.class::isInstance).isPresent()) {
298 MethodSource methodSource = testSource.map(MethodSource.class::cast).get();
299 String realClassName = methodSource.getClassName();
300
301 String[] source = collectAllTestIdentifiersInHierarchy(testIdentifier)
302 .filter(i ->
303 i.getSource().map(ClassSource.class::isInstance).orElse(false))
304 .findFirst()
305 .map(this::toClassMethodName)
306 .map(s -> new String[] {s[0], s[1]})
307 .orElse(new String[] {realClassName, realClassName});
308
309 String parentDisplay = collectAllTestIdentifiersInHierarchy(testIdentifier)
310 .filter(identifier -> identifier
311 .getSource()
312 .filter(MethodSource.class::isInstance)
313 .isPresent())
314 .map(TestIdentifier::getDisplayName)
315 .collect(joining(" "));
316
317 boolean needsSpaceSeparator = isNotBlank(parentDisplay) && !display.startsWith("[");
318 String methodDisplay = parentDisplay + (needsSpaceSeparator ? " " : "") + display;
319
320 boolean hasParameterizedParent = collectAllTestIdentifiersInHierarchy(testIdentifier)
321 .filter(identifier -> !identifier.getSource().isPresent())
322 .map(TestIdentifier::getLegacyReportingName)
323 .anyMatch(legacyReportingName -> legacyReportingName.matches("^\\[.+]$"));
324
325 boolean parameterized = isNotBlank(methodSource.getMethodParameterTypes()) || hasParameterizedParent;
326 String methodName = methodSource.getMethodName();
327 String description = testIdentifier.getLegacyReportingName();
328 boolean equalDescriptions = methodDisplay.equals(description);
329 boolean hasLegacyDescription = description.startsWith(methodName + '(');
330 boolean hasDisplayName = !equalDescriptions || !hasLegacyDescription;
331 String methodDesc = parameterized ? description : methodName;
332 String methodDisp = hasDisplayName ? methodDisplay : methodDesc;
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348 return new String[] {source[0], source[1], methodDesc, methodDisp};
349 } else if (testSource.filter(ClassSource.class::isInstance).isPresent()) {
350 List<String> parentClassDisplays = collectAllTestIdentifiersInHierarchy(testIdentifier)
351 .filter(identifier -> identifier
352 .getSource()
353 .filter(ClassSource.class::isInstance)
354 .isPresent())
355 .map(TestIdentifier::getDisplayName)
356 .collect(toList());
357
358 Collections.reverse(parentClassDisplays);
359 String classDisplay = Stream.concat(parentClassDisplays.stream(), Stream.of(display))
360 .collect(joining(" "));
361
362 ClassSource classSource = testSource.map(ClassSource.class::cast).get();
363 String className = classSource.getClassName();
364 String simpleClassName = className.substring(1 + className.lastIndexOf('.'));
365 String source = classDisplay.replace(' ', '$').equals(simpleClassName) ? className : classDisplay;
366 return new String[] {className, source, null, null};
367 } else {
368 String source = testPlan.getParent(testIdentifier)
369 .map(TestIdentifier::getDisplayName)
370 .orElse(display);
371 return new String[] {source, source, display, display};
372 }
373 }
374
375
376
377
378 Map<TestIdentifier, TestExecutionResult> getFailures() {
379 return failures;
380 }
381
382 boolean hasFailingTests() {
383 return !getFailures().isEmpty();
384 }
385
386 void reset() {
387 getFailures().clear();
388 testPlan = null;
389 }
390
391 @Override
392 public void writeTestOutput(OutputReportEntry reportEntry) {
393 Long testRunId = classMethodIndexer.getLocalIndex();
394 runListener.writeTestOutput(new TestOutputReportEntry(reportEntry, runMode, testRunId));
395 }
396 }