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 org.apache.maven.surefire.api.util.internal.ObjectUtils.systemProps;
25  import static org.apache.maven.surefire.shared.lang3.StringUtils.isNotBlank;
26  import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
27  
28  import java.util.Map;
29  import java.util.Objects;
30  import java.util.Optional;
31  import java.util.concurrent.ConcurrentHashMap;
32  import java.util.concurrent.ConcurrentMap;
33  import java.util.regex.Pattern;
34  
35  import org.apache.maven.surefire.report.PojoStackTraceWriter;
36  import org.apache.maven.surefire.api.report.RunListener;
37  import org.apache.maven.surefire.api.report.SimpleReportEntry;
38  import org.apache.maven.surefire.api.report.StackTraceWriter;
39  import org.junit.platform.engine.TestExecutionResult;
40  import org.junit.platform.engine.TestSource;
41  import org.junit.platform.engine.support.descriptor.ClassSource;
42  import org.junit.platform.engine.support.descriptor.MethodSource;
43  import org.junit.platform.launcher.TestExecutionListener;
44  import org.junit.platform.launcher.TestIdentifier;
45  import org.junit.platform.launcher.TestPlan;
46  
47  /**
48   * @since 2.22.0
49   */
50  final class RunListenerAdapter
51      implements TestExecutionListener
52  {
53      private static final Pattern COMMA_PATTERN = Pattern.compile( "," );
54  
55      private final ConcurrentMap<TestIdentifier, Long> testStartTime = new ConcurrentHashMap<>();
56      private final ConcurrentMap<TestIdentifier, TestExecutionResult> failures = new ConcurrentHashMap<>();
57      private final RunListener runListener;
58      private volatile TestPlan testPlan;
59  
60      RunListenerAdapter( RunListener runListener )
61      {
62          this.runListener = runListener;
63      }
64  
65      @Override
66      public void testPlanExecutionStarted( TestPlan testPlan )
67      {
68          this.testPlan = testPlan;
69      }
70  
71      @Override
72      public void testPlanExecutionFinished( TestPlan testPlan )
73      {
74          this.testPlan = null;
75          testStartTime.clear();
76      }
77  
78      @Override
79      public void executionStarted( TestIdentifier testIdentifier )
80      {
81          if ( testIdentifier.isContainer()
82                          && testIdentifier.getSource().filter( ClassSource.class::isInstance ).isPresent() )
83          {
84              testStartTime.put( testIdentifier, System.currentTimeMillis() );
85              runListener.testSetStarting( createReportEntry( testIdentifier ) );
86          }
87          else if ( testIdentifier.isTest() )
88          {
89              testStartTime.put( testIdentifier, System.currentTimeMillis() );
90              runListener.testStarting( createReportEntry( testIdentifier ) );
91          }
92      }
93  
94      @Override
95      public void executionFinished( TestIdentifier testIdentifier, TestExecutionResult testExecutionResult )
96      {
97          boolean isClass = testIdentifier.isContainer()
98                  && testIdentifier.getSource().filter( ClassSource.class::isInstance ).isPresent();
99  
100         boolean isTest = testIdentifier.isTest();
101 
102         boolean failed = testExecutionResult.getStatus() == FAILED;
103 
104         boolean isAssertionError = testExecutionResult.getThrowable()
105                 .filter( AssertionError.class::isInstance ).isPresent();
106 
107         boolean isRootContainer = testIdentifier.isContainer() && !testIdentifier.getParentId().isPresent();
108 
109         if ( failed || isClass || isTest )
110         {
111             Integer elapsed = computeElapsedTime( testIdentifier );
112             switch ( testExecutionResult.getStatus() )
113             {
114                 case ABORTED:
115                     if ( isTest )
116                     {
117                         runListener.testAssumptionFailure(
118                                 createReportEntry( testIdentifier, testExecutionResult, elapsed ) );
119                     }
120                     else
121                     {
122                         runListener.testSetCompleted( createReportEntry( testIdentifier, testExecutionResult,
123                                 systemProps(), null, elapsed ) );
124                     }
125                     break;
126                 case FAILED:
127                     if ( isAssertionError )
128                     {
129                         runListener.testFailed( createReportEntry( testIdentifier, testExecutionResult, elapsed ) );
130                     }
131                     else
132                     {
133                         runListener.testError( createReportEntry( testIdentifier, testExecutionResult, elapsed ) );
134                     }
135                     if ( isClass || isRootContainer )
136                     {
137                         runListener.testSetCompleted( createReportEntry( testIdentifier, null,
138                                 systemProps(), null, elapsed ) );
139                     }
140                     failures.put( testIdentifier, testExecutionResult );
141                     break;
142                 default:
143                     if ( isTest )
144                     {
145                         runListener.testSucceeded( createReportEntry( testIdentifier, null, elapsed ) );
146                     }
147                     else
148                     {
149                         runListener.testSetCompleted(
150                                 createReportEntry( testIdentifier, null, systemProps(), null, elapsed ) );
151                     }
152             }
153         }
154     }
155 
156     private Integer computeElapsedTime( TestIdentifier testIdentifier )
157     {
158         Long startTime = testStartTime.remove( testIdentifier );
159         long endTime = System.currentTimeMillis();
160         return startTime == null ? null : (int) ( endTime - startTime );
161     }
162 
163     @Override
164     public void executionSkipped( TestIdentifier testIdentifier, String reason )
165     {
166         testStartTime.remove( testIdentifier );
167         runListener.testSkipped( createReportEntry( testIdentifier, null, emptyMap(), reason, null ) );
168     }
169 
170     private SimpleReportEntry createReportEntry( TestIdentifier testIdentifier,
171                                                  TestExecutionResult testExecutionResult,
172                                                  Map<String, String> systemProperties,
173                                                  String reason,
174                                                  Integer elapsedTime )
175     {
176         String[] classMethodName = toClassMethodName( testIdentifier );
177         String className = classMethodName[0];
178         String classText = classMethodName[1];
179         if ( Objects.equals( className, classText ) )
180         {
181             classText = null;
182         }
183         boolean failed = testExecutionResult != null && testExecutionResult.getStatus() == FAILED;
184         String methodName = failed || testIdentifier.isTest() ? classMethodName[2] : null;
185         String methodText = failed || testIdentifier.isTest() ? classMethodName[3] : null;
186         if ( Objects.equals( methodName, methodText ) )
187         {
188             methodText = null;
189         }
190         StackTraceWriter stw =
191                 testExecutionResult == null ? null : toStackTraceWriter( className, methodName, testExecutionResult );
192         return new SimpleReportEntry( className, classText, methodName, methodText,
193                 stw, elapsedTime, reason, systemProperties );
194     }
195 
196     private SimpleReportEntry createReportEntry( TestIdentifier testIdentifier )
197     {
198         return createReportEntry( testIdentifier, null, null );
199     }
200 
201     private SimpleReportEntry createReportEntry( TestIdentifier testIdentifier,
202                                                  TestExecutionResult testExecutionResult, Integer elapsedTime )
203     {
204         return createReportEntry( testIdentifier, testExecutionResult, emptyMap(), null, elapsedTime );
205     }
206 
207     private StackTraceWriter toStackTraceWriter( String realClassName, String realMethodName,
208                                                  TestExecutionResult testExecutionResult )
209     {
210         switch ( testExecutionResult.getStatus() )
211         {
212             case ABORTED:
213             case FAILED:
214                 // Failed tests must have a StackTraceWriter, otherwise Surefire will fail
215                 Throwable exception = testExecutionResult.getThrowable().orElse( null );
216                 return toStackTraceWriter( realClassName, realMethodName, exception );
217             default:
218                 return testExecutionResult.getThrowable()
219                         .map( t -> toStackTraceWriter( realClassName, realMethodName, t ) )
220                         .orElse( null );
221         }
222     }
223 
224     private StackTraceWriter toStackTraceWriter( String realClassName, String realMethodName, Throwable throwable )
225     {
226         return new PojoStackTraceWriter( realClassName, realMethodName, throwable );
227     }
228 
229     /**
230      * <ul>
231      *     <li>[0] class name - used in stacktrace parser</li>
232      *     <li>[1] class display name</li>
233      *     <li>[2] method signature - used in stacktrace parser</li>
234      *     <li>[3] method display name</li>
235      * </ul>
236      *
237      * @param testIdentifier a class or method
238      * @return 4 elements string array
239      */
240     private String[] toClassMethodName( TestIdentifier testIdentifier )
241     {
242         Optional<TestSource> testSource = testIdentifier.getSource();
243         String display = testIdentifier.getDisplayName();
244 
245         if ( testSource.filter( MethodSource.class::isInstance ).isPresent() )
246         {
247             MethodSource methodSource = testSource.map( MethodSource.class::cast ).get();
248             String realClassName = methodSource.getClassName();
249 
250             String[] source = testPlan.getParent( testIdentifier )
251                     .map( this::toClassMethodName )
252                     .map( s -> new String[] { s[0], s[1] } )
253                     .orElse( new String[] { realClassName, realClassName } );
254 
255             String simpleClassNames = COMMA_PATTERN.splitAsStream( methodSource.getMethodParameterTypes() )
256                     .map( s -> s.substring( 1 + s.lastIndexOf( '.' ) ) )
257                     .collect( joining( "," ) );
258 
259             boolean hasParams = isNotBlank( methodSource.getMethodParameterTypes() );
260             String methodName = methodSource.getMethodName();
261             String description = testIdentifier.getLegacyReportingName();
262             String methodSign = hasParams ? methodName + '(' + simpleClassNames + ')' : methodName;
263             boolean equalDescriptions = display.equals( description );
264             boolean hasLegacyDescription = description.startsWith( methodName + '(' );
265             boolean hasDisplayName = !equalDescriptions || !hasLegacyDescription;
266             String methodDesc = equalDescriptions || !hasParams ? methodSign : description;
267             String methodDisp = hasDisplayName ? display : methodDesc;
268 
269             // The behavior of methods getLegacyReportingName() and getDisplayName().
270             //     test      ||  legacy  |  display
271             // ==============||==========|==========
272             //    normal     ||    m()   |    m()
273             //  normal+displ ||   displ  |  displ
274             // parameterized ||  m()[1]  |  displ
275 
276             return new String[] {source[0], source[1], methodDesc, methodDisp};
277         }
278         else if ( testSource.filter( ClassSource.class::isInstance ).isPresent() )
279         {
280             ClassSource classSource = testSource.map( ClassSource.class::cast ).get();
281             String className = classSource.getClassName();
282             String simpleClassName = className.substring( 1 + className.lastIndexOf( '.' ) );
283             String source = display.equals( simpleClassName ) ? className : display;
284             return new String[] {className, source, null, null};
285         }
286         else
287         {
288             String source = testPlan.getParent( testIdentifier )
289                     .map( TestIdentifier::getDisplayName ).orElse( display );
290             return new String[] {source, source, display, display};
291         }
292     }
293 
294     /**
295      * @return Map of tests that failed.
296      */
297     Map<TestIdentifier, TestExecutionResult> getFailures()
298     {
299         return failures;
300     }
301 
302     boolean hasFailingTests()
303     {
304         return !getFailures().isEmpty();
305     }
306 
307     void reset()
308     {
309         getFailures().clear();
310         testPlan = null;
311     }
312 }