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