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