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.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   * @since 2.22.0
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                 // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
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      * <ul>
304      *     <li>[0] class name - used in stacktrace parser</li>
305      *     <li>[1] class display name</li>
306      *     <li>[2] method signature - used in stacktrace parser</li>
307      *     <li>[3] method display name</li>
308      * </ul>
309      *
310      * @param testIdentifier a class or method
311      * @return 4 elements string array
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             // The behavior of methods getLegacyReportingName() and getDisplayName().
352             //     test      ||  legacy  |  display
353             // ==============||==========|==========
354             //    normal     ||    m()   |    m()
355             //  normal+displ ||   displ  |  displ
356             // parameterized ||  m()[1]  |  displ
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      * @return Map of tests that failed.
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 }