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