aboutsummaryrefslogtreecommitdiff
path: root/headless
diff options
context:
space:
mode:
authorGravatar Benjamin Jones <bjones@galois.com>2012-12-07 15:58:32 -0800
committerGravatar Benjamin Jones <bjones@galois.com>2012-12-07 15:58:32 -0800
commitf6e2a45110f0435d9c41ad84477aa03736b24fe8 (patch)
treeee54f4b2d231f30c7274fe03b7fb200b70e9d8c5 /headless
parent6a4f5fbca4137e4aa7bbcaff0bb0c6d07ef7a8a9 (diff)
major refectoring, added in-page CSS to reports, added stats to summary page
Diffstat (limited to 'headless')
-rw-r--r--headless/src/main/java/com/galois/fiveui/Reporter.java335
-rw-r--r--headless/src/test/java/com/galois/fiveui/ReporterTest.java27
2 files changed, 292 insertions, 70 deletions
diff --git a/headless/src/main/java/com/galois/fiveui/Reporter.java b/headless/src/main/java/com/galois/fiveui/Reporter.java
index 3c5e6d5..7fc1a4e 100644
--- a/headless/src/main/java/com/galois/fiveui/Reporter.java
+++ b/headless/src/main/java/com/galois/fiveui/Reporter.java
@@ -1,103 +1,250 @@
+/**
+ * Module : Reporter.java Copyright : (c) 2012, Galois, Inc.
+ *
+ * Maintainer : Stability : Provisional Portability: Portable
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ *
+ * @author Benjamin Jones <bjones@galois.com>
+ */
package com.galois.fiveui;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import com.googlecode.jatl.Html;
+/**
+ * Reporter is responsible for turning a list of results from a FiveUI run and
+ * and generating various reports which summarize the results. The reports are
+ * generated in HTML and returned as human readable strings by getSummaryPage(),
+ * getURLPage(), and getRulePage(). The client is responsible for writing them
+ * to .html files (or whatever).
+ *
+ * @author bjones
+ *
+ */
public class Reporter {
- private String summaryString, byURLString, byRuleString;
+ /** see sortBy* and computeSummaryStats methods for map semantics */
+ private final Map<String, List<String>> _byURLMap;
+ private final Map<String, List<String>> _byRuleMap;
+ private final Map<String, int[]> _passFailMap;
+ private static String GLOBAL_CSS =
+ "h1 { font-size: 150%; }" +
+ "h2 { font-size: 110%; }" +
+ "table { border: 1px solid grey; cellpadding: 5%; width: 200px; }\n" +
+ "td.pass-number{ text-align: right;color: green; }\n" +
+ "td.fail-number{ text-align: right;color: red; }\n" +
+ "td.text{ text-align: left; }\n" +
+ "th { font-weight: bold; }\n" +
+ "td, th { border: 1px solid grey; }\n" +
+ "tr.hlRow { background: #EEEEEE; }\n";
+ /**
+ * Construct a Reporter object. The constructor takes a list of results
+ * and uses them to populate various maps used in reporting.
+ *
+ * @param results a list of Result objects
+ */
public Reporter(List<Result> results) {
- StringWriter summaryPage, byURLPage, byRulePage;
- final Map<String, List<String>> byURLMap;
- final Map<String, List<String>> byRuleMap;
- byURLMap = sortByURL(results);
- byRuleMap = sortByRule(results);
-
- // Build the summary page
- summaryPage = new StringWriter();
- new Html(summaryPage) {{
- bind("foo", "bar");
- html();
- body();
- h1().text("Headless Run Summary").end();
- p();
+ this._byURLMap = sortByURL(results);
+ this._byRuleMap = sortByRule(results);
+ this._passFailMap = computeSummaryStats(results);
+ }
+
+ /**
+ * Build the HTML markup for a summary page based on the precomputed map
+ * this._passFailMap.
+ *
+ * @return String containing human-readable HTML representing a summary page
+ */
+ public String getSummary() {
+ StringWriter summaryPage = new StringWriter();
+ final Map<String, int[]> scopedMap = this._passFailMap;
+ Html page = new Html(summaryPage);
+ page = makeHead(page, "Summary of Results");
+ page = new Html(page) {{
+ h1().text("Headless Run Summary").end();
+ p();
+ ul();
+ li().a().href("byurl.html").text("Results organized by URL").end().end();
+ li().a().href("byrule.html").text("Results organized by Rule").end().end();
+ end();
+ end();
+ p();
+ div().id ("stats");
+ makeSummaryStats(scopedMap);
+ end();
+ end();
+ endAll();
+ done();
+ }
+ /**
+ * Report statistics on the headless run. Note, "pass"
+ * means the URL passed all tests in the ruleset, but "fail" can be
+ * reported for the same test on multiple offenders in the page.
+ */
+ Html makeSummaryStats(Map<String, int[]> passFailMap) {
+ int uniqueURLs = passFailMap.size();
+ int[] passFailList;
+
+ p();
+ h3().text("Unique URLs: ");
+ span().classAttr("number").text(String.valueOf(uniqueURLs)).end();
+ end().end();
+
+ p();
+ table().id("stats-table");
+ tr();
+ th().text("URL").end();
+ th().text("Pass").end();
+ th().text("Fail").end();
+ end();
+ int i = 0; // index for **alternate row highlighting**
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(passFailMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String key: sortedKeys) {
+ passFailList = passFailMap.get(key);
+ tr().classAttr(i % 2 == 0 ? "hlRow" : "regRow");
+ td().classAttr("text").a().href(key).text(key).end().end();
+ td().classAttr("pass-number").text(String.valueOf(passFailList[0])).end();
+ td().classAttr("fail-number").text(String.valueOf(passFailList[1])).end();
+ end();
+ i++;
+ }
+ end(); // end <table>
+ return end(); // end <p>
+ }
+ };
+ return summaryPage.getBuffer().toString();
+ }
+
+ /**
+ * Build the HTML markup for a report page sorted by URL.
+ *
+ * @return String containing human-readable HTML representing a report page
+ */
+ public String getByURL() {
+ StringWriter byURLPage = new StringWriter();
+ final Map<String, List<String>> scopedMap = this._byURLMap;
+ Html page = new Html(byURLPage);
+ page = makeHead(page, "Results sorted by URL");
+ page = new Html(page) {{
+ body();
+ h1().text("Results by URL").end();
+ ol();
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(scopedMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String url: sortedKeys) {
+ li().h2().a().href(url).text(url).end().end();
ul();
- li().a().href("byurl.html").text("by URL").end().end();
- li().a().href("byrule.html").text("by Rule").end().end();
+ for (String entry: scopedMap.get(url)) {
+ li().text(entry).end();
+ }
end();
end();
- p();
- div().id("stats-box");
- text("stats here");
- end();
- end();
- endAll();
- done();
- }};
- this.summaryString = summaryPage.getBuffer().toString();
-
- // Build the "by URL" page
- byURLPage = new StringWriter();
- new Html(byURLPage) {{
- html();
- body();
- h1().text("Results by URL").end();
- ol();
- for (String url: byURLMap.keySet()) {
- li().h2().a().href(url).text(url).end().end();
- ul();
- for (String entry: byURLMap.get(url)) {
- li().text(entry).end();
- }
- end();
- end();
- }
- endAll();
+ }
+ endAll();
done();
}};
- this.byURLString = byURLPage.getBuffer().toString();
-
- // Build the "by Rule" page
- byRulePage = new StringWriter();
- new Html(byRulePage) {{
- html();
- body();
- h1().text("Results by Rule").end();
- ol();
- for (String rule: byRuleMap.keySet()) {
- li().h2().text(rule).end();
- ul();
- for (String url: byRuleMap.get(rule)) {
- li().text("URL: ").a().href(url).text(url).end().end();
- }
- end();
+ return byURLPage.getBuffer().toString();
+ }
+
+ /**
+ * Build the HTML markup for a report page sorted by rule name.
+ *
+ * @return String containing human-readable HTML representing a report page
+ */
+ public String getByRule() {
+ StringWriter byRulePage = new StringWriter();
+ final Map<String, List<String>> scopedMap = this._byRuleMap;
+ Html page = new Html(byRulePage);
+ page = makeHead(page, "Results sorted by rule");
+ page = new Html(page) {{
+ h1().text("Results by Rule").end();
+ ul();
+ List<String> sortedKeys = new ArrayList<String>();
+ sortedKeys.addAll(scopedMap.keySet());
+ Collections.sort(sortedKeys);
+ for (String rule: sortedKeys) {
+ li().h2().text(rule).end();
+ ul();
+ for (String url: scopedMap.get(rule)) {
+ li().a().href(url).text(url).end().end();
+ }
end();
- }
- endAll();
+ end();
+ }
+ endAll();
done();
}};
- this.byRuleString = byRulePage.getBuffer().toString();
+ return byRulePage.getBuffer().toString();
}
- public String getSummary() {
- return summaryString;
+ /**
+ * Utility method to take all the reports and write them to standard file
+ * names under a given directory.
+ *
+ * @param dirName name of the directory where the reports should be written
+ * @throws IOException
+ */
+ public void writeReportsToDir(String dirName) throws IOException {
+ PrintWriter summaryFile = new PrintWriter(new FileWriter(dirName + File.separator + "summary.html"));
+ PrintWriter byURLFile = new PrintWriter(new FileWriter(dirName + File.separator + "byURL.html"));
+ PrintWriter byRuleFile = new PrintWriter(new FileWriter(dirName + File.separator + "byRule.html"));
+ summaryFile.write(this.getSummary());
+ summaryFile.close();
+ byURLFile.write(this.getByURL());
+ byURLFile.close();
+ byRuleFile.write(this.getByRule());
+ byRuleFile.close();
}
- public String getByURL() {
- return byURLString;
- }
+ /** Private fields **/
- public String getByRule() {
- return byRuleString;
+ private Html makeHead(Html page, final String title) {
+ return new Html(page) {{
+ html();
+ head();
+ title().text(title).end();
+ style().type("text/css").text(GLOBAL_CSS).end();
+ end();
+ body();
+ }};
}
+ /**
+ * Populate a Map<String, List<String>> representing the results sorted by
+ * URL.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by URL.
+ */
private Map<String, List<String>>sortByURL(List<Result> results) {
+ /** map semantics: Map< url, [rule1, rule2, ...] > */
Map<String, List<String>> map = new HashMap<String, List<String>>();
String url, rule;
List<String> list;
@@ -118,7 +265,15 @@ public class Reporter {
return map;
}
+ /**
+ * Populate a Map<String, List<String>> representing the results sorted by
+ * rule name.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by rule name.
+ */
private Map<String, List<String>>sortByRule(List<Result> results) {
+ /** map semantics: Map< rule, [url1, url2, ...] > */
Map<String, List<String>> map = new HashMap<String, List<String>>();
String url, rule;
List<String> list;
@@ -137,4 +292,44 @@ public class Reporter {
return map;
}
+ /**
+ * Compute summary statistics from the results list. This includes number of
+ * passes and fails for each URL checked.
+ *
+ * @param results a list of results
+ * @return a map representing the results sorted by rule name.
+ */
+ private Map<String, int[]> computeSummaryStats(List<Result> results) {
+ /** passFailMap semantics: Map<url, {#pass, #fail}> */
+ Map<String, int[]> passFailMap = new HashMap<String, int[]>();
+ String url;
+ int pass, fail;
+ int[] passFailList;
+ Pattern numberPassedPattern = Pattern.compile("passed ([0-9]+) tests");
+ Matcher matcher;
+
+ for (Result result : results) {
+ pass = fail = 0;
+ url = result.getURL();
+ if (result.getType() == ResType.Pass) {
+ // now we have to parse out how many tests passed
+ matcher = numberPassedPattern.matcher(result.getDesc());
+ if (matcher.find())
+ pass = Integer.parseInt(matcher.group(1)); // throws exception if parse fails
+ } else if (result.getType() == ResType.Error) {
+ // each error result corresponds to one test
+ fail = 1;
+ }
+
+ if (passFailMap.containsKey(url)) {
+ passFailList = passFailMap.get(url);
+ } else {
+ passFailList = new int[] { 0, 0};
+ passFailMap.put(url, passFailList);
+ }
+ passFailList[0] += pass;
+ passFailList[1] += fail;
+ }
+ return passFailMap;
+ }
}
diff --git a/headless/src/test/java/com/galois/fiveui/ReporterTest.java b/headless/src/test/java/com/galois/fiveui/ReporterTest.java
index 88be37b..8509c5b 100644
--- a/headless/src/test/java/com/galois/fiveui/ReporterTest.java
+++ b/headless/src/test/java/com/galois/fiveui/ReporterTest.java
@@ -2,11 +2,14 @@ package com.galois.fiveui;
import static org.junit.Assert.*;
+import java.io.File;
+import java.io.IOException;
import org.junit.Test;
import com.google.common.collect.ImmutableList;
public class ReporterTest {
+
@Test
public void testConstructor() {
ImmutableList<Result> r = ImmutableList.of(
@@ -21,4 +24,28 @@ public class ReporterTest {
kermit.getByURL().length() > 0 &&
kermit.getByRule().length() > 0);
}
+
+ @Test
+ public void testSummaryPage() throws IOException {
+ //File tmpPath = Files.createTempDir();
+ File tmpPath = new File("/tmp/");
+ System.out.println("Writing test summary page to: " + tmpPath.toString() + File.separator);
+ ImmutableList<Result> r = ImmutableList.of(
+ Result.pass(null, "OK", "http://nonexistant", "test rule 1"),
+ Result.pass(null, "OK", "http://intransigent", "test rule 1"),
+ Result.pass(null, "OK", "http://intransigent", "test rule 3"),
+ Result.pass(null, "OK", "http://intransigent", "test rule 4"),
+ Result.pass(null, "OK", "http://intransigent", "test rule 5"),
+ Result.pass(null, "OK", "http://foo.com", "test rule 1"),
+ Result.error(null, "ERROR", "http://foo.com", "test rule 5"),
+ Result.error(null, "ERROR", "http://foo.com", "test rule 2"),
+ Result.error(null, "ERROR", "http://bar.com", "test rule 3"),
+ Result.error(null, "ERROR", "http://bar.com", "test rule 3"), // multiple fails for same url+rule combo
+ Result.error(null, "ERROR", "http://bar.com", "test rule 3"),
+ Result.error(null, "ERROR", "http://bar.com", "test rule 3"),
+ Result.error(null, "ERROR", "http://nonexistant", "test rule 2"));
+ Reporter kermit = new Reporter(r);
+ kermit.writeReportsToDir(tmpPath.toString());
+ assertTrue("made it!", true);
+ }
}