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   * <p/>
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      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     public RunResult close()
163     {
164         mergeTestHistoryResult();
165         runCompleted();
166         for ( TestSetRunListener listener : listeners )
167         {
168             listener.close();
169         }
170         return globalStats.getRunResult();
171     }
172 
173     public void runStarting()
174     {
175         log( "" );
176         log( "-------------------------------------------------------" );
177         log( " T E S T S" );
178         log( "-------------------------------------------------------" );
179     }
180 
181     private void runCompleted()
182     {
183         if ( reportConfiguration.isPrintSummary() )
184         {
185             log( "" );
186             log( "Results:" );
187             log( "" );
188         }
189         boolean printedFailures = printTestFailures( failure );
190         boolean printedErrors = printTestFailures( error );
191         boolean printedFlakes = printTestFailures( flake );
192         if ( printedFailures | printedErrors | printedFlakes )
193         {
194             log( "" );
195         }
196         boolean hasSuccessful = globalStats.getCompletedCount() > 0;
197         boolean hasSkipped = globalStats.getSkipped() > 0;
198         log( globalStats.getSummary(), hasSuccessful, printedFailures, printedErrors, hasSkipped, printedFlakes );
199         log( "" );
200     }
201 
202     public RunStatistics getGlobalRunStatistics()
203     {
204         mergeTestHistoryResult();
205         return globalStats;
206     }
207 
208     /**
209      * For testing purposes only.
210      */
211     public static DefaultReporterFactory defaultNoXml()
212     {
213         return new DefaultReporterFactory( StartupReportConfiguration.defaultNoXml(), new NullConsoleLogger() );
214     }
215 
216     /**
217      * Get the result of a test based on all its runs. If it has success and failures/errors, then it is a flake;
218      * if it only has errors or failures, then count its result based on its first run
219      *
220      * @param reportEntries the list of test run report type for a given test
221      * @param rerunFailingTestsCount configured rerun count for failing tests
222      * @return the type of test result
223      */
224     // Use default visibility for testing
225     static TestResultType getTestResultType( List<ReportEntryType> reportEntries, int rerunFailingTestsCount  )
226     {
227         if ( reportEntries == null || reportEntries.isEmpty() )
228         {
229             return unknown;
230         }
231 
232         boolean seenSuccess = false, seenFailure = false, seenError = false;
233         for ( ReportEntryType resultType : reportEntries )
234         {
235             if ( resultType == SUCCESS )
236             {
237                 seenSuccess = true;
238             }
239             else if ( resultType == FAILURE )
240             {
241                 seenFailure = true;
242             }
243             else if ( resultType == ERROR )
244             {
245                 seenError = true;
246             }
247         }
248 
249         if ( seenFailure || seenError )
250         {
251             if ( seenSuccess && rerunFailingTestsCount > 0 )
252             {
253                 return flake;
254             }
255             else
256             {
257                 if ( seenError )
258                 {
259                     return error;
260                 }
261                 else
262                 {
263                     return failure;
264                 }
265             }
266         }
267         else if ( seenSuccess )
268         {
269             return success;
270         }
271         else
272         {
273             return skipped;
274         }
275     }
276 
277     /**
278      * Merge all the TestMethodStats in each TestRunListeners and put results into flakyTests, failedTests and
279      * errorTests, indexed by test class and method name. Update globalStatistics based on the result of the merge.
280      */
281     void mergeTestHistoryResult()
282     {
283         globalStats = new RunStatistics();
284         flakyTests = new TreeMap<String, List<TestMethodStats>>();
285         failedTests = new TreeMap<String, List<TestMethodStats>>();
286         errorTests = new TreeMap<String, List<TestMethodStats>>();
287 
288         Map<String, List<TestMethodStats>> mergedTestHistoryResult = new HashMap<String, List<TestMethodStats>>();
289         // Merge all the stats for tests from listeners
290         for ( TestSetRunListener listener : listeners )
291         {
292             List<TestMethodStats> testMethodStats = listener.getTestMethodStats();
293             for ( TestMethodStats methodStats : testMethodStats )
294             {
295                 List<TestMethodStats> currentMethodStats =
296                     mergedTestHistoryResult.get( methodStats.getTestClassMethodName() );
297                 if ( currentMethodStats == null )
298                 {
299                     currentMethodStats = new ArrayList<TestMethodStats>();
300                     currentMethodStats.add( methodStats );
301                     mergedTestHistoryResult.put( methodStats.getTestClassMethodName(), currentMethodStats );
302                 }
303                 else
304                 {
305                     currentMethodStats.add( methodStats );
306                 }
307             }
308         }
309 
310         // Update globalStatistics by iterating through mergedTestHistoryResult
311         int completedCount = 0, skipped = 0;
312 
313         for ( Map.Entry<String, List<TestMethodStats>> entry : mergedTestHistoryResult.entrySet() )
314         {
315             List<TestMethodStats> testMethodStats = entry.getValue();
316             String testClassMethodName = entry.getKey();
317             completedCount++;
318 
319             List<ReportEntryType> resultTypes = new ArrayList<ReportEntryType>();
320             for ( TestMethodStats methodStats : testMethodStats )
321             {
322                 resultTypes.add( methodStats.getResultType() );
323             }
324 
325             switch ( getTestResultType( resultTypes, reportConfiguration.getRerunFailingTestsCount() ) )
326             {
327                 case success:
328                     // If there are multiple successful runs of the same test, count all of them
329                     int successCount = 0;
330                     for ( ReportEntryType type : resultTypes )
331                     {
332                         if ( type == SUCCESS )
333                         {
334                             successCount++;
335                         }
336                     }
337                     completedCount += successCount - 1;
338                     break;
339                 case skipped:
340                     skipped++;
341                     break;
342                 case flake:
343                     flakyTests.put( testClassMethodName, testMethodStats );
344                     break;
345                 case failure:
346                     failedTests.put( testClassMethodName, testMethodStats );
347                     break;
348                 case error:
349                     errorTests.put( testClassMethodName, testMethodStats );
350                     break;
351                 default:
352                     throw new IllegalStateException( "Get unknown test result type" );
353             }
354         }
355 
356         globalStats.set( completedCount, errorTests.size(), failedTests.size(), skipped, flakyTests.size() );
357     }
358 
359     /**
360      * Print failed tests and flaked tests. A test is considered as a failed test if it failed/got an error with
361      * all the runs. If a test passes in ever of the reruns, it will be count as a flaked test
362      *
363      * @param type   the type of results to be printed, could be error, failure or flake
364      * @return {@code true} if printed some lines
365      */
366     // Use default visibility for testing
367     boolean printTestFailures( TestResultType type )
368     {
369         final Map<String, List<TestMethodStats>> testStats;
370         final Level level;
371         switch ( type )
372         {
373             case failure:
374                 testStats = failedTests;
375                 level = Level.FAILURE;
376                 break;
377             case error:
378                 testStats = errorTests;
379                 level = Level.FAILURE;
380                 break;
381             case flake:
382                 testStats = flakyTests;
383                 level = Level.UNSTABLE;
384                 break;
385             default:
386                 return false;
387         }
388 
389         boolean printed = false;
390         if ( !testStats.isEmpty() )
391         {
392             log( type.getLogPrefix(), level );
393             printed = true;
394         }
395 
396         for ( Map.Entry<String, List<TestMethodStats>> entry : testStats.entrySet() )
397         {
398             printed = true;
399             List<TestMethodStats> testMethodStats = entry.getValue();
400             if ( testMethodStats.size() == 1 )
401             {
402                 // No rerun, follow the original output format
403                 failure( "  " + testMethodStats.get( 0 ).getStackTraceWriter().smartTrimmedStackTrace() );
404             }
405             else
406             {
407                 log( entry.getKey(), level );
408                 for ( int i = 0; i < testMethodStats.size(); i++ )
409                 {
410                     StackTraceWriter failureStackTrace = testMethodStats.get( i ).getStackTraceWriter();
411                     if ( failureStackTrace == null )
412                     {
413                         success( "  Run " + ( i + 1 ) + ": PASS" );
414                     }
415                     else
416                     {
417                         failure( "  Run " + ( i + 1 ) + ": " + failureStackTrace.smartTrimmedStackTrace() );
418                     }
419                 }
420                 log( "" );
421             }
422         }
423         return printed;
424     }
425 
426     // Describe the result of a given test
427     enum TestResultType
428     {
429 
430         error(   "Errors: "   ),
431         failure( "Failures: " ),
432         flake(   "Flakes: "   ),
433         success( "Success: "  ),
434         skipped( "Skipped: "  ),
435         unknown( "Unknown: "  );
436 
437         private final String logPrefix;
438 
439         TestResultType( String logPrefix )
440         {
441             this.logPrefix = logPrefix;
442         }
443 
444         public String getLogPrefix()
445         {
446             return logPrefix;
447         }
448     }
449 
450     private void log( String s, boolean success, boolean failures, boolean errors, boolean skipped, boolean flakes )
451     {
452         Level level = resolveLevel( success, failures, errors, skipped, flakes );
453         log( s, level );
454     }
455 
456     private void log( String s, Level level )
457     {
458         MessageBuilder builder = buffer();
459         switch ( level )
460         {
461             case FAILURE:
462                 consoleLogger.error( builder.failure( s ).toString() );
463                 break;
464             case UNSTABLE:
465                 consoleLogger.warning( builder.warning( s ).toString() );
466                 break;
467             case SUCCESS:
468                 consoleLogger.info( builder.success( s ).toString() );
469                 break;
470             default:
471                 consoleLogger.info( builder.a( s ).toString() );
472         }
473     }
474 
475     private void log( String s )
476     {
477         consoleLogger.info( s );
478     }
479 
480     private void info( String s )
481     {
482         MessageBuilder builder = buffer();
483         consoleLogger.info( builder.info( s ).toString() );
484     }
485 
486     private void err( String s )
487     {
488         MessageBuilder builder = buffer();
489         consoleLogger.error( builder.error( s ).toString() );
490     }
491 
492     private void success( String s )
493     {
494         MessageBuilder builder = buffer();
495         consoleLogger.info( builder.success( s ).toString() );
496     }
497 
498     private void failure( String s )
499     {
500         MessageBuilder builder = buffer();
501         consoleLogger.error( builder.failure( s ).toString() );
502     }
503 }