View Javadoc
1   package org.apache.maven.plugin.surefire.report;
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 org.apache.maven.plugin.surefire.StartupReportConfiguration;
23  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
24  import org.apache.maven.plugin.surefire.log.api.Level;
25  import org.apache.maven.plugin.surefire.runorder.StatisticsReporter;
26  import org.apache.maven.surefire.shared.utils.logging.MessageBuilder;
27  import org.apache.maven.surefire.extensions.ConsoleOutputReportEventListener;
28  import org.apache.maven.surefire.extensions.StatelessReportEventListener;
29  import org.apache.maven.surefire.extensions.StatelessTestsetInfoConsoleReportEventListener;
30  import org.apache.maven.surefire.extensions.StatelessTestsetInfoFileReportEventListener;
31  import org.apache.maven.surefire.api.report.ReporterFactory;
32  import org.apache.maven.surefire.api.report.RunListener;
33  import org.apache.maven.surefire.report.RunStatistics;
34  import org.apache.maven.surefire.api.report.StackTraceWriter;
35  import org.apache.maven.surefire.api.suite.RunResult;
36  
37  import java.io.File;
38  import java.util.ArrayList;
39  import java.util.Collection;
40  import java.util.HashMap;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.TreeMap;
44  import java.util.concurrent.ConcurrentLinkedQueue;
45  
46  import static org.apache.maven.plugin.surefire.log.api.Level.resolveLevel;
47  import static org.apache.maven.plugin.surefire.report.ConsoleReporter.PLAIN;
48  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.error;
49  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.failure;
50  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.flake;
51  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.skipped;
52  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.success;
53  import static org.apache.maven.plugin.surefire.report.DefaultReporterFactory.TestResultType.unknown;
54  import static org.apache.maven.plugin.surefire.report.ReportEntryType.ERROR;
55  import static org.apache.maven.plugin.surefire.report.ReportEntryType.FAILURE;
56  import static org.apache.maven.plugin.surefire.report.ReportEntryType.SUCCESS;
57  import static org.apache.maven.surefire.shared.utils.logging.MessageUtils.buffer;
58  import static org.apache.maven.surefire.api.util.internal.ObjectUtils.useNonNull;
59  
60  /**
61   * Provides reporting modules on the plugin side.
62   * <br>
63   * Keeps a centralized count of test run results.
64   *
65   * @author Kristian Rosenvold
66   */
67  public class DefaultReporterFactory
68      implements ReporterFactory
69  {
70      private final Collection<TestSetRunListener> listeners = new ConcurrentLinkedQueue<>();
71      private final StartupReportConfiguration reportConfiguration;
72      private final ConsoleLogger consoleLogger;
73      private final Integer forkNumber;
74  
75      private RunStatistics globalStats = new RunStatistics();
76  
77      // from "<testclass>.<testmethod>" -> statistics about all the runs for flaky tests
78      private Map<String, List<TestMethodStats>> flakyTests;
79  
80      // from "<testclass>.<testmethod>" -> statistics about all the runs for failed tests
81      private Map<String, List<TestMethodStats>> failedTests;
82  
83      // from "<testclass>.<testmethod>" -> statistics about all the runs for error tests
84      private Map<String, List<TestMethodStats>> errorTests;
85  
86      public DefaultReporterFactory( StartupReportConfiguration reportConfiguration, ConsoleLogger consoleLogger )
87      {
88          this( reportConfiguration, consoleLogger, null );
89      }
90  
91      public DefaultReporterFactory( StartupReportConfiguration reportConfiguration, ConsoleLogger consoleLogger,
92                                     Integer forkNumber )
93      {
94          this.reportConfiguration = reportConfiguration;
95          this.consoleLogger = consoleLogger;
96          this.forkNumber = forkNumber;
97      }
98  
99      @Override
100     public RunListener createReporter()
101     {
102         TestSetRunListener testSetRunListener =
103             new TestSetRunListener( createConsoleReporter(),
104                                     createFileReporter(),
105                                     createSimpleXMLReporter(),
106                                     createConsoleOutputReceiver(),
107                                     createStatisticsReporter(),
108                                     reportConfiguration.isTrimStackTrace(),
109                                     PLAIN.equals( reportConfiguration.getReportFormat() ),
110                                     reportConfiguration.isBriefOrPlainFormat() );
111         addListener( testSetRunListener );
112         return testSetRunListener;
113     }
114 
115     public File getReportsDirectory()
116     {
117         return reportConfiguration.getReportsDirectory();
118     }
119 
120     private StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> createConsoleReporter()
121     {
122         StatelessTestsetInfoConsoleReportEventListener<WrappedReportEntry, TestSetStats> consoleReporter =
123                 reportConfiguration.instantiateConsoleReporter( consoleLogger );
124         return useNonNull( consoleReporter, NullConsoleReporter.INSTANCE );
125     }
126 
127     private StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> createFileReporter()
128     {
129         StatelessTestsetInfoFileReportEventListener<WrappedReportEntry, TestSetStats> fileReporter =
130                 reportConfiguration.instantiateFileReporter( forkNumber );
131         return useNonNull( fileReporter, NullFileReporter.INSTANCE );
132     }
133 
134     private StatelessReportEventListener<WrappedReportEntry, TestSetStats> createSimpleXMLReporter()
135     {
136         StatelessReportEventListener<WrappedReportEntry, TestSetStats> xmlReporter =
137                 reportConfiguration.instantiateStatelessXmlReporter( forkNumber );
138         return useNonNull( xmlReporter, NullStatelessXmlReporter.INSTANCE );
139     }
140 
141     private ConsoleOutputReportEventListener createConsoleOutputReceiver()
142     {
143         ConsoleOutputReportEventListener outputReporter =
144                 reportConfiguration.instantiateConsoleOutputFileReporter( forkNumber );
145         return useNonNull( outputReporter, NullConsoleOutputReceiver.INSTANCE );
146     }
147 
148     private StatisticsReporter createStatisticsReporter()
149     {
150         StatisticsReporter statisticsReporter = reportConfiguration.getStatisticsReporter();
151         return useNonNull( statisticsReporter, NullStatisticsReporter.INSTANCE );
152     }
153 
154     public void mergeFromOtherFactories( Collection<DefaultReporterFactory> factories )
155     {
156         for ( DefaultReporterFactory factory : factories )
157         {
158             listeners.addAll( factory.listeners );
159         }
160     }
161 
162     final void addListener( TestSetRunListener listener )
163     {
164         listeners.add( listener );
165     }
166 
167     @Override
168     public RunResult close()
169     {
170         mergeTestHistoryResult();
171         runCompleted();
172         for ( TestSetRunListener listener : listeners )
173         {
174             listener.close();
175         }
176         return globalStats.getRunResult();
177     }
178 
179     public void runStarting()
180     {
181         if ( reportConfiguration.isPrintSummary() )
182         {
183             log( "" );
184             log( "-------------------------------------------------------" );
185             log( " T E S T S" );
186             log( "-------------------------------------------------------" );
187         }
188     }
189 
190     private void runCompleted()
191     {
192         if ( reportConfiguration.isPrintSummary() )
193         {
194             log( "" );
195             log( "Results:" );
196             log( "" );
197         }
198         boolean printedFailures = printTestFailures( failure );
199         boolean printedErrors = printTestFailures( error );
200         boolean printedFlakes = printTestFailures( flake );
201         if ( reportConfiguration.isPrintSummary() )
202         {
203             if ( printedFailures | printedErrors | printedFlakes )
204             {
205                 log( "" );
206             }
207             boolean hasSuccessful = globalStats.getCompletedCount() > 0;
208             boolean hasSkipped = globalStats.getSkipped() > 0;
209             log( globalStats.getSummary(), hasSuccessful, printedFailures, printedErrors, hasSkipped, printedFlakes );
210             log( "" );
211         }
212     }
213 
214     public RunStatistics getGlobalRunStatistics()
215     {
216         mergeTestHistoryResult();
217         return globalStats;
218     }
219 
220     /**
221      * Get the result of a test based on all its runs. If it has success and failures/errors, then it is a flake;
222      * if it only has errors or failures, then count its result based on its first run
223      *
224      * @param reportEntries the list of test run report type for a given test
225      * @param rerunFailingTestsCount configured rerun count for failing tests
226      * @return the type of test result
227      */
228     // Use default visibility for testing
229     static TestResultType getTestResultType( List<ReportEntryType> reportEntries, int rerunFailingTestsCount  )
230     {
231         if ( reportEntries == null || reportEntries.isEmpty() )
232         {
233             return unknown;
234         }
235 
236         boolean seenSuccess = false, seenFailure = false, seenError = false;
237         for ( ReportEntryType resultType : reportEntries )
238         {
239             if ( resultType == SUCCESS )
240             {
241                 seenSuccess = true;
242             }
243             else if ( resultType == FAILURE )
244             {
245                 seenFailure = true;
246             }
247             else if ( resultType == ERROR )
248             {
249                 seenError = true;
250             }
251         }
252 
253         if ( seenFailure || seenError )
254         {
255             if ( seenSuccess && rerunFailingTestsCount > 0 )
256             {
257                 return flake;
258             }
259             else
260             {
261                 return seenError ? error : failure;
262             }
263         }
264         else if ( seenSuccess )
265         {
266             return success;
267         }
268         else
269         {
270             return skipped;
271         }
272     }
273 
274     /**
275      * Merge all the TestMethodStats in each TestRunListeners and put results into flakyTests, failedTests and
276      * errorTests, indexed by test class and method name. Update globalStatistics based on the result of the merge.
277      */
278     private void mergeTestHistoryResult()
279     {
280         globalStats = new RunStatistics();
281         flakyTests = new TreeMap<>();
282         failedTests = new TreeMap<>();
283         errorTests = new TreeMap<>();
284 
285         Map<String, List<TestMethodStats>> mergedTestHistoryResult = new HashMap<>();
286         // Merge all the stats for tests from listeners
287         for ( TestSetRunListener listener : listeners )
288         {
289             for ( TestMethodStats methodStats : listener.getTestMethodStats() )
290             {
291                 List<TestMethodStats> currentMethodStats =
292                     mergedTestHistoryResult.get( methodStats.getTestClassMethodName() );
293                 if ( currentMethodStats == null )
294                 {
295                     currentMethodStats = new ArrayList<>();
296                     currentMethodStats.add( methodStats );
297                     mergedTestHistoryResult.put( methodStats.getTestClassMethodName(), currentMethodStats );
298                 }
299                 else
300                 {
301                     currentMethodStats.add( methodStats );
302                 }
303             }
304         }
305 
306         // Update globalStatistics by iterating through mergedTestHistoryResult
307         int completedCount = 0, skipped = 0;
308 
309         for ( Map.Entry<String, List<TestMethodStats>> entry : mergedTestHistoryResult.entrySet() )
310         {
311             List<TestMethodStats> testMethodStats = entry.getValue();
312             String testClassMethodName = entry.getKey();
313             completedCount++;
314 
315             List<ReportEntryType> resultTypes = new ArrayList<>();
316             for ( TestMethodStats methodStats : testMethodStats )
317             {
318                 resultTypes.add( methodStats.getResultType() );
319             }
320 
321             switch ( getTestResultType( resultTypes, reportConfiguration.getRerunFailingTestsCount() ) )
322             {
323                 case success:
324                     // If there are multiple successful runs of the same test, count all of them
325                     int successCount = 0;
326                     for ( ReportEntryType type : resultTypes )
327                     {
328                         if ( type == SUCCESS )
329                         {
330                             successCount++;
331                         }
332                     }
333                     completedCount += successCount - 1;
334                     break;
335                 case skipped:
336                     skipped++;
337                     break;
338                 case flake:
339                     flakyTests.put( testClassMethodName, testMethodStats );
340                     break;
341                 case failure:
342                     failedTests.put( testClassMethodName, testMethodStats );
343                     break;
344                 case error:
345                     errorTests.put( testClassMethodName, testMethodStats );
346                     break;
347                 default:
348                     throw new IllegalStateException( "Get unknown test result type" );
349             }
350         }
351 
352         globalStats.set( completedCount, errorTests.size(), failedTests.size(), skipped, flakyTests.size() );
353     }
354 
355     /**
356      * Print failed tests and flaked tests. A test is considered as a failed test if it failed/got an error with
357      * all the runs. If a test passes in ever of the reruns, it will be count as a flaked test
358      *
359      * @param type   the type of results to be printed, could be error, failure or flake
360      * @return {@code true} if printed some lines
361      */
362     // Use default visibility for testing
363     boolean printTestFailures( TestResultType type )
364     {
365         final Map<String, List<TestMethodStats>> testStats;
366         final Level level;
367         switch ( type )
368         {
369             case failure:
370                 testStats = failedTests;
371                 level = Level.FAILURE;
372                 break;
373             case error:
374                 testStats = errorTests;
375                 level = Level.FAILURE;
376                 break;
377             case flake:
378                 testStats = flakyTests;
379                 level = Level.UNSTABLE;
380                 break;
381             default:
382                 return false;
383         }
384 
385         boolean printed = false;
386         if ( !testStats.isEmpty() )
387         {
388             log( type.getLogPrefix(), level );
389             printed = true;
390         }
391 
392         for ( Map.Entry<String, List<TestMethodStats>> entry : testStats.entrySet() )
393         {
394             List<TestMethodStats> testMethodStats = entry.getValue();
395             if ( testMethodStats.size() == 1 )
396             {
397                 // No rerun, follow the original output format
398                 failure( "  " + testMethodStats.get( 0 ).getStackTraceWriter().smartTrimmedStackTrace() );
399             }
400             else
401             {
402                 log( entry.getKey(), level );
403                 for ( int i = 0; i < testMethodStats.size(); i++ )
404                 {
405                     StackTraceWriter failureStackTrace = testMethodStats.get( i ).getStackTraceWriter();
406                     if ( failureStackTrace == null )
407                     {
408                         success( "  Run " + ( i + 1 ) + ": PASS" );
409                     }
410                     else
411                     {
412                         failure( "  Run " + ( i + 1 ) + ": " + failureStackTrace.smartTrimmedStackTrace() );
413                     }
414                 }
415                 log( "" );
416             }
417         }
418         return printed;
419     }
420 
421     // Describe the result of a given test
422     enum TestResultType
423     {
424 
425         error(   "Errors: "   ),
426         failure( "Failures: " ),
427         flake(   "Flakes: "   ),
428         success( "Success: "  ),
429         skipped( "Skipped: "  ),
430         unknown( "Unknown: "  );
431 
432         private final String logPrefix;
433 
434         TestResultType( String logPrefix )
435         {
436             this.logPrefix = logPrefix;
437         }
438 
439         public String getLogPrefix()
440         {
441             return logPrefix;
442         }
443     }
444 
445     private void log( String s, boolean success, boolean failures, boolean errors, boolean skipped, boolean flakes )
446     {
447         Level level = resolveLevel( success, failures, errors, skipped, flakes );
448         log( s, level );
449     }
450 
451     private void log( String s, Level level )
452     {
453         switch ( level )
454         {
455             case FAILURE:
456                 failure( s );
457                 break;
458             case UNSTABLE:
459                 warning( s );
460                 break;
461             case SUCCESS:
462                 success( s );
463                 break;
464             default:
465                 info( s );
466         }
467     }
468 
469     private void log( String s )
470     {
471         consoleLogger.info( s );
472     }
473 
474     private void info( String s )
475     {
476         MessageBuilder builder = buffer();
477         consoleLogger.info( builder.a( s ).toString() );
478     }
479 
480     private void warning( String s )
481     {
482         MessageBuilder builder = buffer();
483         consoleLogger.warning( builder.warning( s ).toString() );
484     }
485 
486     private void success( String s )
487     {
488         MessageBuilder builder = buffer();
489         consoleLogger.info( builder.success( s ).toString() );
490     }
491 
492     private void failure( String s )
493     {
494         MessageBuilder builder = buffer();
495         consoleLogger.error( builder.failure( s ).toString() );
496     }
497 }