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