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