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