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