View Javadoc
1   package org.apache.maven.surefire.junitplatform;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
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   * @since 2.22.0
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                 // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
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      * <ul>
301      *     <li>[0] class name - used in stacktrace parser</li>
302      *     <li>[1] class display name</li>
303      *     <li>[2] method signature - used in stacktrace parser</li>
304      *     <li>[3] method display name</li>
305      * </ul>
306      *
307      * @param testIdentifier a class or method
308      * @return 4 elements string array
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             // The behavior of methods getLegacyReportingName() and getDisplayName().
352             //     junit4    ||  legacy  |  display
353             // ==============||==========|==========
354             //     normal    ||     m    |     m
355             //     param     ||   m[0]   |   m[0]
356             //  param+displ  || m[displ] | m[displ]
357 
358             //     junit5    ||  legacy  |  display
359             // ==============||==========|==========
360             //    normal     ||    m()   |    m()
361             //  normal+displ ||    m()   |   displ
362             //     param     ||  m()[1]  | [1] <param>
363             //  param+displ  ||  m()[1]  |   displ
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      * @return Map of tests that failed.
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 }