View Javadoc
1   package org.apache.maven.plugins.invoker;
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 java.io.File;
23  import java.io.IOException;
24  import java.text.DecimalFormat;
25  import java.text.DecimalFormatSymbols;
26  import java.text.MessageFormat;
27  import java.text.NumberFormat;
28  import java.util.ArrayList;
29  import java.util.List;
30  import java.util.Locale;
31  
32  import org.apache.maven.doxia.sink.Sink;
33  import org.apache.maven.doxia.siterenderer.Renderer;
34  import org.apache.maven.plugins.invoker.model.BuildJob;
35  import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Reader;
36  import org.apache.maven.plugins.annotations.Component;
37  import org.apache.maven.plugins.annotations.Mojo;
38  import org.apache.maven.plugins.annotations.Parameter;
39  import org.apache.maven.project.MavenProject;
40  import org.apache.maven.reporting.AbstractMavenReport;
41  import org.apache.maven.reporting.MavenReportException;
42  import org.codehaus.plexus.i18n.I18N;
43  import org.codehaus.plexus.util.ReaderFactory;
44  import org.codehaus.plexus.util.StringUtils;
45  import org.codehaus.plexus.util.xml.XmlStreamReader;
46  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
47  
48  /**
49   * Generate a report based on the results of the Maven invocations. <strong>Note:</strong> This mojo doesn't fork any
50   * lifecycle, if you have a clean working copy, you have to use a command like
51   * <code>mvn clean integration-test site</code> to ensure the build results are present when this goal is invoked.
52   *
53   * @author Olivier Lamy
54   * @since 1.4
55   */
56  @Mojo( name = "report", threadSafe = true )
57  public class InvokerReport
58      extends AbstractMavenReport
59  {
60  
61      /**
62       * The Maven Project.
63       */
64      @Parameter( defaultValue = "${project}", readonly = true, required = true )
65      protected MavenProject project;
66  
67      /**
68       * Doxia Site Renderer component.
69       */
70      @Component
71      protected Renderer siteRenderer;
72  
73      /**
74       * Internationalization component.
75       */
76      @Component
77      protected I18N i18n;
78  
79      /**
80       * The output directory for the report. Note that this parameter is only evaluated if the goal is run directly from
81       * the command line. If the goal is run indirectly as part of a site generation, the output directory configured in
82       * the Maven Site Plugin is used instead.
83       */
84      @Parameter( defaultValue = "${project.reporting.outputDirectory}", required = true )
85      protected File outputDirectory;
86  
87      /**
88       * Base directory where all build reports have been written to.
89       */
90      @Parameter( defaultValue = "${project.build.directory}/invoker-reports", property = "invoker.reportsDirectory" )
91      private File reportsDirectory;
92  
93      /**
94       * The number format used to print percent values in the report locale.
95       */
96      private NumberFormat percentFormat;
97  
98      /**
99       * The number format used to print time values in the report locale.
100      */
101     private NumberFormat secondsFormat;
102 
103     /**
104      * The format used to print build name and description.
105      */
106     private MessageFormat nameAndDescriptionFormat;
107 
108     protected void executeReport( Locale locale )
109         throws MavenReportException
110     {
111         DecimalFormatSymbols symbols = new DecimalFormatSymbols( locale );
112         percentFormat = new DecimalFormat( getText( locale, "report.invoker.format.percent" ), symbols );
113         secondsFormat = new DecimalFormat( getText( locale, "report.invoker.format.seconds" ), symbols );
114         nameAndDescriptionFormat =
115                 new MessageFormat( getText( locale, "report.invoker.format.name_with_description" ) );
116 
117         Sink sink = getSink();
118 
119         sink.head();
120 
121         sink.title();
122         sink.text( getText( locale, "report.invoker.result.title" ) );
123         sink.title_();
124 
125         sink.head_();
126 
127         sink.body();
128 
129         sink.section1();
130         sink.sectionTitle1();
131         sink.text( getText( locale, "report.invoker.result.title" ) );
132         sink.sectionTitle1_();
133         sink.paragraph();
134         sink.text( getText( locale, "report.invoker.result.description" ) );
135         sink.paragraph_();
136         sink.section1_();
137 
138         // ----------------------------------
139         // build buildJob beans
140         // ----------------------------------
141         File[] reportFiles = ReportUtils.getReportFiles( reportsDirectory );
142         if ( reportFiles.length <= 0 )
143         {
144             getLog().info( "no invoker report files found, skip report generation" );
145             return;
146         }
147 
148         BuildJobXpp3Reader buildJobReader = new BuildJobXpp3Reader();
149 
150         List<BuildJob> buildJobs = new ArrayList<>( reportFiles.length );
151         for ( File reportFile : reportFiles )
152         {
153             try ( XmlStreamReader xmlReader = ReaderFactory.newXmlReader( reportFile ) )
154             {
155                 buildJobs.add( buildJobReader.read( xmlReader ) );
156             }
157             catch ( XmlPullParserException e )
158             {
159                 throw new MavenReportException( "Failed to parse report file: " + reportFile, e );
160             }
161             catch ( IOException e )
162             {
163                 throw new MavenReportException( "Failed to read report file: " + reportFile, e );
164             }
165         }
166 
167         // ----------------------------------
168         // summary
169         // ----------------------------------
170 
171         constructSummarySection( buildJobs, locale );
172 
173         // ----------------------------------
174         // per file/it detail
175         // ----------------------------------
176 
177         sink.section2();
178         sink.sectionTitle2();
179 
180         sink.text( getText( locale, "report.invoker.detail.title" ) );
181 
182         sink.sectionTitle2_();
183 
184         sink.section2_();
185 
186         // detail tests table header
187         sink.table();
188 
189         sink.tableRow();
190         // -------------------------------------------
191         // name | Result | time | message
192         // -------------------------------------------
193         sinkTableHeader( sink, getText( locale, "report.invoker.detail.name" ) );
194         sinkTableHeader( sink, getText( locale, "report.invoker.detail.result" ) );
195         sinkTableHeader( sink, getText( locale, "report.invoker.detail.time" ) );
196         sinkTableHeader( sink, getText( locale, "report.invoker.detail.message" ) );
197 
198         sink.tableRow_();
199 
200         for ( BuildJob buildJob : buildJobs )
201         {
202             renderBuildJob( buildJob );
203         }
204 
205         sink.table_();
206 
207         sink.body_();
208 
209         sink.flush();
210         sink.close();
211     }
212 
213     private void constructSummarySection( List<? extends BuildJob> buildJobs, Locale locale )
214     {
215         Sink sink = getSink();
216 
217         sink.section2();
218         sink.sectionTitle2();
219 
220         sink.text( getText( locale, "report.invoker.summary.title" ) );
221 
222         sink.sectionTitle2_();
223         sink.section2_();
224 
225         // ------------------------------------------------------------------------
226         // Building a table with
227         // it number | succes nb | failed nb | Success rate | total time | avg time
228         // ------------------------------------------------------------------------
229 
230         sink.table();
231         sink.tableRow();
232 
233         sinkTableHeader( sink, getText( locale, "report.invoker.summary.number" ) );
234         sinkTableHeader( sink, getText( locale, "report.invoker.summary.success" ) );
235         sinkTableHeader( sink, getText( locale, "report.invoker.summary.failed" ) );
236         sinkTableHeader( sink, getText( locale, "report.invoker.summary.skipped" ) );
237         sinkTableHeader( sink, getText( locale, "report.invoker.summary.success.rate" ) );
238         sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.total" ) );
239         sinkTableHeader( sink, getText( locale, "report.invoker.summary.time.avg" ) );
240 
241         int number = buildJobs.size();
242         int success = 0;
243         int failed = 0;
244         int skipped = 0;
245         double totalTime = 0;
246 
247         for ( BuildJob buildJob : buildJobs )
248         {
249             if ( BuildJob.Result.SUCCESS.equals( buildJob.getResult() ) )
250             {
251                 success++;
252             }
253             else if ( BuildJob.Result.SKIPPED.equals( buildJob.getResult() ) )
254             {
255                 skipped++;
256             }
257             else
258             {
259                 failed++;
260             }
261             totalTime += buildJob.getTime();
262         }
263 
264         sink.tableRow_();
265         sink.tableRow();
266 
267         sinkCell( sink, Integer.toString( number ) );
268         sinkCell( sink, Integer.toString( success ) );
269         sinkCell( sink, Integer.toString( failed ) );
270         sinkCell( sink, Integer.toString( skipped ) );
271 
272         if ( success + failed > 0 )
273         {
274             sinkCell( sink, percentFormat.format( (double) success / ( success + failed ) ) );
275         }
276         else
277         {
278             sinkCell( sink, "" );
279         }
280 
281         sinkCell( sink, secondsFormat.format( totalTime ) );
282 
283         sinkCell( sink, secondsFormat.format( totalTime / number ) );
284 
285         sink.tableRow_();
286         sink.table_();
287 
288     }
289 
290     private void renderBuildJob( BuildJob buildJob )
291     {
292         Sink sink = getSink();
293         sink.tableRow();
294         sinkCell( sink, getBuildJobReportName( buildJob ) );
295         // FIXME image
296         sinkCell( sink, buildJob.getResult() );
297         sinkCell( sink, secondsFormat.format( buildJob.getTime() ) );
298         sinkCell( sink, buildJob.getFailureMessage() );
299         sink.tableRow_();
300     }
301 
302     private String getBuildJobReportName( BuildJob buildJob )
303     {
304         String buildJobName = buildJob.getName();
305         String buildJobDescription = buildJob.getDescription();
306         boolean emptyJobName = StringUtils.isEmpty( buildJobName );
307         boolean emptyJobDescription = StringUtils.isEmpty( buildJobDescription );
308         boolean isReportJobNameComplete = !emptyJobName && !emptyJobDescription;
309         if ( isReportJobNameComplete )
310         {
311             return getFormattedName( buildJobName, buildJobDescription );
312         }
313         else
314         {
315             String buildJobProject = buildJob.getProject();
316             if ( !emptyJobName )
317             {
318                 getLog().warn( incompleteNameWarning( "description", buildJobProject ) );
319             }
320             else if ( !emptyJobDescription )
321             {
322                 getLog().warn( incompleteNameWarning( "name", buildJobProject ) );
323             }
324             return buildJobProject;
325         }
326     }
327 
328     private static String incompleteNameWarning( String missing, String pom )
329     {
330         return String.format( "Incomplete job name-description: %s is missing. "
331                             + "POM (%s) will be used in place of job name.",
332                               missing, pom );
333     }
334 
335     private String getFormattedName( String name, String description )
336     {
337         return nameAndDescriptionFormat.format( new Object[] { name, description } );
338     }
339 
340     protected String getOutputDirectory()
341     {
342         return outputDirectory.getAbsolutePath();
343     }
344 
345     protected MavenProject getProject()
346     {
347         return project;
348     }
349 
350     protected Renderer getSiteRenderer()
351     {
352         return siteRenderer;
353     }
354 
355     public String getDescription( Locale locale )
356     {
357         return getText( locale, "report.invoker.result.description" );
358     }
359 
360     public String getName( Locale locale )
361     {
362         return getText( locale, "report.invoker.result.name" );
363     }
364 
365     public String getOutputName()
366     {
367         return "invoker-report";
368     }
369 
370     public boolean canGenerateReport()
371     {
372         return ReportUtils.getReportFiles( reportsDirectory ).length > 0;
373     }
374 
375     private String getText( Locale locale, String key )
376     {
377         return i18n.getString( "invoker-report", locale, key );
378     }
379 
380     private void sinkTableHeader( Sink sink, String header )
381     {
382         sink.tableHeaderCell();
383         sink.text( header );
384         sink.tableHeaderCell_();
385     }
386 
387     private void sinkCell( Sink sink, String text )
388     {
389         sink.tableCell();
390         sink.text( text );
391         sink.tableCell_();
392     }
393 
394 }