View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.invoker;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.text.DecimalFormat;
24  import java.text.DecimalFormatSymbols;
25  import java.text.MessageFormat;
26  import java.text.NumberFormat;
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.Locale;
30  
31  import org.apache.maven.doxia.sink.Sink;
32  import org.apache.maven.plugins.annotations.Component;
33  import org.apache.maven.plugins.annotations.Mojo;
34  import org.apache.maven.plugins.annotations.Parameter;
35  import org.apache.maven.plugins.invoker.model.BuildJob;
36  import org.apache.maven.plugins.invoker.model.io.xpp3.BuildJobXpp3Reader;
37  import org.apache.maven.reporting.AbstractMavenReport;
38  import org.apache.maven.reporting.MavenReportException;
39  import org.codehaus.plexus.i18n.I18N;
40  import org.codehaus.plexus.util.ReaderFactory;
41  import org.codehaus.plexus.util.StringUtils;
42  import org.codehaus.plexus.util.xml.XmlStreamReader;
43  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
44  
45  /**
46   * Generate a report based on the results of the Maven invocations. <strong>Note:</strong> This mojo doesn't fork any
47   * lifecycle, if you have a clean working copy, you have to use a command like
48   * <code>mvn clean integration-test site</code> to ensure the build results are present when this goal is invoked.
49   *
50   * @author Olivier Lamy
51   * @since 1.4
52   */
53  @Mojo(name = "report", threadSafe = true)
54  public class InvokerReport extends AbstractMavenReport {
55  
56      /**
57       * Internationalization component.
58       */
59      @Component
60      protected I18N i18n;
61  
62      /**
63       * Base directory where all build reports have been written to.
64       */
65      @Parameter(defaultValue = "${project.build.directory}/invoker-reports", property = "invoker.reportsDirectory")
66      private File reportsDirectory;
67  
68      /**
69       * The number format used to print percent values in the report locale.
70       */
71      private NumberFormat percentFormat;
72  
73      /**
74       * The number format used to print time values in the report locale.
75       */
76      private NumberFormat secondsFormat;
77  
78      /**
79       * The format used to print build name and description.
80       */
81      private MessageFormat nameAndDescriptionFormat;
82  
83      protected void executeReport(Locale locale) throws MavenReportException {
84          DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
85          percentFormat = new DecimalFormat(getText(locale, "report.invoker.format.percent"), symbols);
86          secondsFormat = new DecimalFormat(getText(locale, "report.invoker.format.seconds"), symbols);
87          nameAndDescriptionFormat = new MessageFormat(getText(locale, "report.invoker.format.name_with_description"));
88  
89          Sink sink = getSink();
90  
91          sink.head();
92  
93          sink.title();
94          sink.text(getText(locale, "report.invoker.result.title"));
95          sink.title_();
96  
97          sink.head_();
98  
99          sink.body();
100 
101         sink.section1();
102         sink.sectionTitle1();
103         sink.text(getText(locale, "report.invoker.result.title"));
104         sink.sectionTitle1_();
105         sink.paragraph();
106         sink.text(getText(locale, "report.invoker.result.description"));
107         sink.paragraph_();
108         sink.section1_();
109 
110         // ----------------------------------
111         // build buildJob beans
112         // ----------------------------------
113         File[] reportFiles = ReportUtils.getReportFiles(reportsDirectory);
114         if (reportFiles.length <= 0) {
115             getLog().info("no invoker report files found, skip report generation");
116             return;
117         }
118 
119         BuildJobXpp3Reader buildJobReader = new BuildJobXpp3Reader();
120 
121         List<BuildJob> buildJobs = new ArrayList<>(reportFiles.length);
122         for (File reportFile : reportFiles) {
123             try (XmlStreamReader xmlReader = ReaderFactory.newXmlReader(reportFile)) {
124                 buildJobs.add(buildJobReader.read(xmlReader));
125             } catch (XmlPullParserException e) {
126                 throw new MavenReportException("Failed to parse report file: " + reportFile, e);
127             } catch (IOException e) {
128                 throw new MavenReportException("Failed to read report file: " + reportFile, e);
129             }
130         }
131 
132         // ----------------------------------
133         // summary
134         // ----------------------------------
135 
136         constructSummarySection(buildJobs, locale);
137 
138         // ----------------------------------
139         // per file/it detail
140         // ----------------------------------
141 
142         sink.section2();
143         sink.sectionTitle2();
144 
145         sink.text(getText(locale, "report.invoker.detail.title"));
146 
147         sink.sectionTitle2_();
148 
149         sink.section2_();
150 
151         // detail tests table header
152         sink.table();
153         sink.tableRows(null, false);
154 
155         sink.tableRow();
156         // -------------------------------------------
157         // name | Result | time | message
158         // -------------------------------------------
159         sinkTableHeader(sink, getText(locale, "report.invoker.detail.name"));
160         sinkTableHeader(sink, getText(locale, "report.invoker.detail.result"));
161         sinkTableHeader(sink, getText(locale, "report.invoker.detail.time"));
162         sinkTableHeader(sink, getText(locale, "report.invoker.detail.message"));
163 
164         sink.tableRow_();
165 
166         for (BuildJob buildJob : buildJobs) {
167             renderBuildJob(buildJob);
168         }
169 
170         sink.tableRows_();
171         sink.table_();
172 
173         sink.body_();
174 
175         sink.flush();
176         sink.close();
177     }
178 
179     private void constructSummarySection(List<? extends BuildJob> buildJobs, Locale locale) {
180         Sink sink = getSink();
181 
182         sink.section2();
183         sink.sectionTitle2();
184 
185         sink.text(getText(locale, "report.invoker.summary.title"));
186 
187         sink.sectionTitle2_();
188         sink.section2_();
189 
190         // ------------------------------------------------------------------------
191         // Building a table with
192         // it number | succes nb | failed nb | Success rate | total time | avg time
193         // ------------------------------------------------------------------------
194 
195         sink.table();
196         sink.tableRows(null, false);
197 
198         sink.tableRow();
199 
200         sinkTableHeader(sink, getText(locale, "report.invoker.summary.number"));
201         sinkTableHeader(sink, getText(locale, "report.invoker.summary.success"));
202         sinkTableHeader(sink, getText(locale, "report.invoker.summary.failed"));
203         sinkTableHeader(sink, getText(locale, "report.invoker.summary.skipped"));
204         sinkTableHeader(sink, getText(locale, "report.invoker.summary.success.rate"));
205         sinkTableHeader(sink, getText(locale, "report.invoker.summary.time.total"));
206         sinkTableHeader(sink, getText(locale, "report.invoker.summary.time.avg"));
207 
208         int number = buildJobs.size();
209         int success = 0;
210         int failed = 0;
211         int skipped = 0;
212         double totalTime = 0;
213 
214         for (BuildJob buildJob : buildJobs) {
215             if (BuildJob.Result.SUCCESS.equals(buildJob.getResult())) {
216                 success++;
217             } else if (BuildJob.Result.SKIPPED.equals(buildJob.getResult())) {
218                 skipped++;
219             } else {
220                 failed++;
221             }
222             totalTime += buildJob.getTime();
223         }
224 
225         sink.tableRow_();
226         sink.tableRow();
227 
228         sinkCell(sink, Integer.toString(number));
229         sinkCell(sink, Integer.toString(success));
230         sinkCell(sink, Integer.toString(failed));
231         sinkCell(sink, Integer.toString(skipped));
232 
233         if (success + failed > 0) {
234             sinkCell(sink, percentFormat.format((double) success / (success + failed)));
235         } else {
236             sinkCell(sink, "");
237         }
238 
239         sinkCell(sink, secondsFormat.format(totalTime));
240 
241         sinkCell(sink, secondsFormat.format(totalTime / number));
242 
243         sink.tableRow_();
244 
245         sink.tableRows_();
246         sink.table_();
247     }
248 
249     private void renderBuildJob(BuildJob buildJob) {
250         Sink sink = getSink();
251         sink.tableRow();
252         sinkCell(sink, getBuildJobReportName(buildJob));
253         // FIXME image
254         sinkCell(sink, buildJob.getResult());
255         sinkCell(sink, secondsFormat.format(buildJob.getTime()));
256         sinkCell(sink, buildJob.getFailureMessage());
257         sink.tableRow_();
258     }
259 
260     private String getBuildJobReportName(BuildJob buildJob) {
261         String buildJobName = buildJob.getName();
262         String buildJobDescription = buildJob.getDescription();
263         boolean emptyJobName = StringUtils.isEmpty(buildJobName);
264         boolean emptyJobDescription = StringUtils.isEmpty(buildJobDescription);
265         boolean isReportJobNameComplete = !emptyJobName && !emptyJobDescription;
266         if (isReportJobNameComplete) {
267             return getFormattedName(buildJobName, buildJobDescription);
268         } else {
269             String buildJobProject = buildJob.getProject();
270             if (!emptyJobName) {
271                 getLog().warn(incompleteNameWarning("description", buildJobProject));
272             } else if (!emptyJobDescription) {
273                 getLog().warn(incompleteNameWarning("name", buildJobProject));
274             }
275             return buildJobProject;
276         }
277     }
278 
279     private static String incompleteNameWarning(String missing, String pom) {
280         return String.format(
281                 "Incomplete job name-description: %s is missing. " + "POM (%s) will be used in place of job name.",
282                 missing, pom);
283     }
284 
285     private String getFormattedName(String name, String description) {
286         return nameAndDescriptionFormat.format(new Object[] {name, description});
287     }
288 
289     public String getDescription(Locale locale) {
290         return getText(locale, "report.invoker.result.description");
291     }
292 
293     public String getName(Locale locale) {
294         return getText(locale, "report.invoker.result.name");
295     }
296 
297     public String getOutputName() {
298         return "invoker-report";
299     }
300 
301     public boolean canGenerateReport() {
302         return ReportUtils.getReportFiles(reportsDirectory).length > 0;
303     }
304 
305     private String getText(Locale locale, String key) {
306         return i18n.getString("invoker-report", locale, key);
307     }
308 
309     private void sinkTableHeader(Sink sink, String header) {
310         sink.tableHeaderCell();
311         sink.text(header);
312         sink.tableHeaderCell_();
313     }
314 
315     private void sinkCell(Sink sink, String text) {
316         sink.tableCell();
317         sink.text(text);
318         sink.tableCell_();
319     }
320 }