View Javadoc

1   package com.google.code.jetm.maven;
2   
3   import static org.fest.assertions.Assertions.assertThat;
4   
5   import java.io.File;
6   import java.io.FileReader;
7   import java.io.IOException;
8   import java.io.InputStream;
9   import java.io.StringReader;
10  import java.net.URI;
11  import java.net.URL;
12  import java.text.DecimalFormat;
13  import java.util.Arrays;
14  import java.util.Collection;
15  import java.util.Iterator;
16  import java.util.LinkedList;
17  import java.util.List;
18  import java.util.Properties;
19  import java.util.ResourceBundle;
20  
21  import org.apache.commons.io.IOUtils;
22  import org.apache.maven.shared.invoker.InvocationResult;
23  import org.apache.maven.shared.test.plugin.BuildTool;
24  import org.codehaus.plexus.util.FileUtils;
25  import org.jdom.Attribute;
26  import org.jdom.Document;
27  import org.jdom.Element;
28  import org.jdom.filter.ElementFilter;
29  import org.jdom.input.SAXBuilder;
30  import org.junit.After;
31  import org.junit.AfterClass;
32  import org.junit.Before;
33  import org.junit.BeforeClass;
34  import org.junit.Rule;
35  import org.junit.Test;
36  import org.junit.rules.TestName;
37  import org.openqa.selenium.By;
38  import org.openqa.selenium.WebDriver;
39  import org.openqa.selenium.WebElement;
40  import org.openqa.selenium.htmlunit.HtmlUnitDriver;
41  
42  import com.google.code.jetm.maven.data.TimeUnit;
43  import com.google.code.jetm.maven.internal.SimpleAggregate;
44  import com.google.code.jetm.reporting.xml.XmlAggregateBinder;
45  
46  import etm.core.aggregation.Aggregate;
47  
48  /**
49   * Integration tests for the timing report mojo.
50   * 
51   * @author jrh3k5
52   * 
53   */
54  
55  public class TimingReportMojoITest {
56      /**
57       * A {@link Rule} used to retrieve the test name.
58       */
59      @Rule
60      public TestName testName = new TestName();
61  
62      private static final Properties originalSystemProperties = System.getProperties();
63      private final List<String> cleanTestSite = Arrays.asList("clean", "test", "site");
64      private WebDriver driver;
65      private BuildTool build;
66  
67      /**
68       * Set the {@code $maven.home} value in the system properties for the
69       * {@link BuildTool} object.
70       * 
71       * @throws Exception
72       *             If any errors occur during the setup.
73       */
74      @BeforeClass
75      public static void setUpBeforeClass() throws Exception {
76          final ResourceBundle systemPropsBundle = ResourceBundle.getBundle("system");
77          System.setProperty("maven.home", systemPropsBundle.getString("maven.home"));
78      }
79  
80      /**
81       * Create and initialize a {@link BuildTool} object to invoke Maven.
82       * 
83       * @throws Exception
84       *             If any errors occur during the setup.
85       */
86      @Before
87      public void setUp() throws Exception {
88          build = new BuildTool();
89          build.initialize();
90  
91          driver = new HtmlUnitDriver();
92      }
93  
94      /**
95       * Clean up resources used by the (possibly) instantiated {@link BuildTool}
96       * object.
97       * 
98       * @throws Exception
99       *             If any errors occur during the teardown.
100      */
101     @After
102     public void tearDown() throws Exception {
103         if (build != null)
104             build.dispose();
105 
106         if (driver != null)
107             driver.close();
108     }
109 
110     /**
111      * Restore the system properties to their original values.
112      * 
113      * @throws Exception
114      *             If any errors occur during the teardown.
115      */
116     @AfterClass
117     public static void tearDownAfterClass() throws Exception {
118         System.setProperties(originalSystemProperties);
119     }
120 
121     /**
122      * Test the creation of a report for a demo project.
123      * 
124      * @throws Exception
125      *             If any errors occur during the test run.
126      */
127     @SuppressWarnings("unchecked")
128     @Test
129     public void testDemoProject() throws Exception {
130         final String projectName = "demo-project";
131         final File logFile = getLogFile();
132         final InvocationResult result = build.executeMaven(getPom(projectName), null, cleanTestSite, logFile);
133         assertThat(result.getExitCode()).isZero();
134     
135         final WebDriver driver = getDriver();
136         driver.get(getSiteIndexLocation(projectName));
137         openJetmTimingReport(driver);
138     
139         final Collection<Aggregate> reportData = getAggregateData();
140         for (File reportFile : (List<File>) FileUtils.getFiles(FileUtils.toFile(getClass().getResource("/example-projects/" + projectName + "/target/jetm")), "**/*.xml", null, true)) {
141             final Collection<Aggregate> aggregates = getAggregates(reportFile, TimeUnit.SECONDS);
142             // Find the entry for the file
143             assertHasSection(reportFile.getName());
144             // Make sure the data's in the report
145             assertThat(reportData).contains(aggregates.toArray());
146         }
147     }
148 
149     /**
150      * Test the creation of an empty report.
151      * 
152      * @throws Exception
153      *             If any errors occur during the test run.
154      */
155     @Test
156     public void testEmptyReport() throws Exception {
157         final String projectName = "empty-project";
158         final File logFile = getLogFile();
159         final InvocationResult result = build.executeMaven(getPom(projectName), null, cleanTestSite, logFile);
160         assertThat(result.getExitCode()).isZero();
161 
162         final File siteIndex = new File(URI.create(getSiteIndexLocation(projectName)));
163         assertThat(new File(siteIndex.getParentFile(), "jetm-timing-report.html")).doesNotExist();
164     }
165 
166     /**
167      * Versions of maven-site-plugin past 2.0.x bring in a newer version of
168      * doxia which made for stricter requirements in the usage of the doxia API.
169      * This manifested in the {@code <th />} tags being incorrectly generated
170      * and placed outside of the {@code <table />} tag.
171      * 
172      * @throws Exception
173      *             If any errors occur during the test run.
174      */
175     @Test
176     public void testMavenSitePluginCompatibility() throws Exception {
177         final String projectName = "maven-site-plugin-version";
178         final File baseLogFile = getLogFile();
179 
180         for (String sitePluginVersion : new String[] { "2.0", "2.1", "2.2" }) {
181             final File logFile = new File(baseLogFile.getParentFile(), sitePluginVersion + "-" + baseLogFile.getName());
182 
183             final Properties mavenProps = new Properties();
184             mavenProps.setProperty("site.plugin.version", sitePluginVersion);
185 
186             final InvocationResult result = build.executeMaven(getPom(projectName), mavenProps, cleanTestSite, logFile);
187             assertThat(result.getExitCode()).as("Execution for version " + sitePluginVersion + " failed.").isZero();
188 
189             final InputStream resourceStream = getClass().getResourceAsStream("/example-projects/maven-site-plugin-version/target/site/jetm-timing-report.html");
190             assertThat(resourceStream).as("Could not find timing report for plugin version " + sitePluginVersion).isNotNull();
191             try {
192                 final Document document = getDocument(IOUtils.toString(resourceStream));
193                 @SuppressWarnings("unchecked")
194                 final Iterator<Element> headerElements = document.getDescendants(new ElementFilter("th"));
195                 assertThat(headerElements.hasNext()).as("No header elements in version " + sitePluginVersion).isTrue();
196                 while (headerElements.hasNext()) {
197                     final Element tableHeader = headerElements.next();
198                     final Element tableRow = tableHeader.getParentElement();
199                     assertThat(tableRow.getName()).isEqualTo("tr");
200 
201                     /*
202                      * doxia used by 2.0 wraps the body of the table in a <tbody
203                      * /> tag
204                      */
205                     Element tableElement = null;
206                     if ("2.0".equals(sitePluginVersion)) {
207                         final Element tableBody = tableRow.getParentElement();
208                         assertThat(tableBody.getName()).isEqualTo("tbody");
209                         assertThat((tableElement = tableBody.getParentElement()).getName()).isEqualTo("table");
210                     } else
211                         assertThat((tableElement = tableRow.getParentElement()).getName()).isEqualTo("table");
212 
213                     /*
214                      * To ensure some consistent styling, there should be zero
215                      * border
216                      */
217                     final Attribute borderAttribute = tableElement.getAttribute("border");
218                     if (borderAttribute != null)
219                         assertThat(borderAttribute.getValue()).isEqualTo("0");
220                 }
221             } finally {
222                 IOUtils.closeQuietly(resourceStream);
223             }
224 
225             driver.close();
226         }
227     }
228 
229     /**
230      * Test the rendering of a report in milliseconds.
231      * 
232      * @throws Exception
233      *             If any errors occur during the test run.
234      */
235     @SuppressWarnings("unchecked")
236     @Test
237     public void testRenderInMilliseconds() throws Exception {
238         final String projectName = "millisecond-project";
239         final File logFile = getLogFile();
240         final InvocationResult result = build.executeMaven(getPom(projectName), null, cleanTestSite, logFile);
241         assertThat(result.getExitCode()).isZero();
242 
243         final WebDriver driver = getDriver();
244         driver.get(getSiteIndexLocation(projectName));
245         openJetmTimingReport(driver);
246 
247         final Collection<Aggregate> reportData = getAggregateData();
248         for (File reportFile : (List<File>) FileUtils.getFiles(FileUtils.toFile(getClass().getResource("/example-projects/" + projectName + "/target/jetm")), "**/*.xml", null, true)) {
249             final Collection<Aggregate> aggregates = getAggregates(reportFile, TimeUnit.MILLISECONDS);
250             // Find the entry for the file
251             assertHasSection(reportFile.getName());
252             // Make sure the data's in the report
253             assertThat(reportData).contains(aggregates.toArray());
254         }
255     }
256 
257     /**
258      * Assert that the section by the given name exists in the report.
259      * 
260      * @param sectionName
261      *            The section name whose existence is to be verified.
262      */
263     private void assertHasSection(String sectionName) {
264         for (WebElement element : driver.findElements(By.tagName("h4")))
265             if (sectionName.equals(element.getText()))
266                 return;
267 
268         throw new IllegalArgumentException("No section name found: " + sectionName + ". Page source: " + driver.getPageSource());
269     }
270 
271     /**
272      * Parse aggregate data from the report.
273      * 
274      * @return A {@link Collection} of {@link Aggregate} objects representing
275      *         the data in the report.
276      */
277     private Collection<Aggregate> getAggregateData() {
278         final Collection<Aggregate> aggregates = new LinkedList<Aggregate>();
279         final List<WebElement> htmlRows = driver.findElements(By.tagName("tr"));
280         for (WebElement htmlRow : htmlRows) {
281             final List<WebElement> htmlCells = htmlRow.findElements(By.tagName("td"));
282             if (htmlCells.size() == 6) {
283                 final double average = Double.parseDouble(htmlCells.get(1).getText());
284                 final double min = Double.parseDouble(htmlCells.get(3).getText());
285                 final double max = Double.parseDouble(htmlCells.get(4).getText());
286                 final double total = Double.parseDouble(htmlCells.get(5).getText());
287                 final long measurements = Long.parseLong(htmlCells.get(2).getText());
288                 aggregates.add(new SimpleAggregate(htmlCells.get(0).getText(), average, min, max, measurements, total));
289             }
290         }
291         return aggregates;
292     }
293 
294     /**
295      * Attempt to parse a given XML document into a JDOM document object. This
296      * method tries five times, because there seem to be intermittent issues
297      * with reading these generated HTML files - inability to read XSD files,
298      * unexpected end of file, and other fun things. In the event that all
299      * attempts have been exhausted, all of the collected exceptions will be
300      * printed and then another exception will be thrown to interrupt the test
301      * execution.
302      * <p />
303      * Admittedly, this is a hack, but it's a workaround to unstable sources of
304      * information (such as the XSD) that should not cause the test to fail.
305      * 
306      * @param xml
307      *            The XML document that is to be parsed.
308      * @return A {@link Document} representing the given XML document.
309      * @throws IllegalStateException
310      *             If all attempts to parse the document are exhausted.
311      */
312     private Document getDocument(String xml) {
313         final int maxAttempts = 5;
314         final Exception[] caught = new Exception[maxAttempts];
315         for (int i = 0; i < maxAttempts; i++) {
316             try {
317                 return new SAXBuilder().build(new StringReader(xml));
318             } catch (Exception e) {
319                 caught[i] = e;
320             }
321         }
322 
323         for (int i = 0; i < caught.length && caught[i] != null; i++)
324             caught[i].printStackTrace();
325 
326         throw new IllegalStateException("Too many errors were encountered while trying to parse the document.");
327     }
328 
329     /**
330      * Get a web driver to explore the generated Maven site.
331      * 
332      * @return A {@link WebDriver}. Each invocation will close whatever driver
333      *         was previously returned and return an entirely new driver.
334      */
335     private WebDriver getDriver() {
336         if (driver != null)
337             driver.close();
338 
339         return driver = new HtmlUnitDriver();
340     }
341 
342     /**
343      * Get the aggregate data from the given file.
344      * 
345      * @param aggregateData
346      *            A {@link File} containing the raw aggregate data to be
347      *            collected.
348      * @return A {@link Collection} of {@link Aggregate} data parsed from the
349      *         given file and rounded to a precision as displayed in the report.
350      * @throws IOException
351      *             If any errors occur during the collection of the data.
352      */
353     private Collection<Aggregate> getAggregates(File aggregateData, TimeUnit timeUnit) throws IOException {
354         final FileReader reader = new FileReader(aggregateData);
355         try {
356             final Collection<Aggregate> aggregates = new LinkedList<Aggregate>();
357             for (Aggregate original : new XmlAggregateBinder().unbind(reader)) {
358                 /*
359                  * Divide each by a thousand to convert from milliseconds to
360                  * seconds
361                  */
362                 final double average = round(original.getAverage() / timeUnit.getDivisionValue());
363                 final double min = round(original.getMin() / timeUnit.getDivisionValue());
364                 final double max = round(original.getMax() / timeUnit.getDivisionValue());
365                 final double total = round(original.getTotal() / timeUnit.getDivisionValue());
366                 aggregates.add(new SimpleAggregate(original.getName(), average, min, max, original.getMeasurements(), total));
367             }
368             return aggregates;
369         } finally {
370             reader.close();
371         }
372     }
373 
374     /**
375      * Get a log file to be used in the Maven build.
376      * 
377      * @return A {@link File} to be used to store Maven build output.
378      * @throws IOException
379      *             If any errors occur while generating the log file path.
380      */
381     private File getLogFile() throws IOException {
382         final File logFile = new File("target/failsafe-reports/" + testName.getMethodName() + "-maven.log");
383         FileUtils.forceMkdir(logFile.getParentFile());
384         return logFile;
385     }
386 
387     /**
388      * Retrieve a POM file.
389      * 
390      * @param artifactId
391      *            The artifact ID of the project whose POM is to be retrieved.
392      * @return A {@link File} reference to the given artifact ID's POM.
393      * @throws IllegalArgumentException
394      *             If the POM cannot be found.
395      */
396     private File getPom(String artifactId) {
397         final URL pomUrl = getClass().getResource("/example-projects/" + artifactId + "/pom.xml");
398         if (pomUrl == null)
399             throw new IllegalArgumentException("No POM found for artifact: " + artifactId);
400         return FileUtils.toFile(pomUrl);
401     }
402 
403     /**
404      * Get the location of the index.html for a Maven project's site.
405      * 
406      * @param artifactId
407      *            The artifact ID of the project whose site index is to be
408      *            retrieved.
409      * @return A {@code String} representing the absolute location of the site
410      *         index.
411      * @throws IllegalArgumentException
412      *             If the site index cannot be found.
413      */
414     private String getSiteIndexLocation(String artifactId) {
415         final URL pomUrl = getClass().getResource("/example-projects/" + artifactId + "/target/site/index.html");
416         if (pomUrl == null)
417             throw new IllegalArgumentException("No index.html found for artifact: " + artifactId);
418         return pomUrl.toExternalForm();
419     }
420 
421     /**
422      * Open the JETM timing report.
423      * 
424      * @param driver
425      *            The {@link WebDriver} to use.
426      */
427     private void openJetmTimingReport(WebDriver driver) {
428         driver.findElement(By.linkText("Project Reports")).click();
429         driver.findElement(By.linkText("JETM Timing Report")).click();
430     }
431 
432     /**
433      * Round a floating point value to two decimal places.
434      * 
435      * @param value
436      *            The decimal value to be rounded.
437      * @return The given floating point value, rounded to two places.
438      */
439     private double round(double value) {
440         return Double.parseDouble(new DecimalFormat("0.00").format(value));
441     }
442 }