aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jesse Hallett <jesse@galois.com>2014-01-07 14:10:05 -0800
committerGravatar Jesse Hallett <jesse@galois.com>2014-01-07 15:49:13 -0800
commitbb7a486e5876d9bd3c4bf8a18f45a30baf5f8fe5 (patch)
treecacf319105ac465109dab883effaf2436b9aa1d4
parent38a352620d98efca076e42828e48ca2617d7ea6d (diff)
Displays table of errors and warnings by rule
- Splits "failures" column into "errors" and "warnings" - Links go to detailed report instead of to source page
-rw-r--r--src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java216
1 files changed, 155 insertions, 61 deletions
diff --git a/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java b/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java
index 344e46d..486e5c3 100644
--- a/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java
+++ b/src/batchtools/headless/src/main/java/com/galois/fiveui/Reporter.java
@@ -24,6 +24,8 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -32,6 +34,7 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.googlecode.jatl.Html;
@@ -50,16 +53,19 @@ public class Reporter {
/** see sortBy* and computeSummaryStats methods for map semantics */
private final Map<String, List<Result>> _byURLMap;
private final Map<String, List<Result>> _byRuleMap;
- private final Map<String, int[]> _passFailMap;
+ private final Map<String, int[]> _statsByUrl;
+ private final Map<String, int[]> _statsByRule;
private Map<String, String> _ruleNameToDesc;
private static String GLOBAL_CSS = "h1 { font-size: 150%; }"
+ "h2 { font-size: 110%; }"
+ "li { margin: 5 0 0 0; }"
- + "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"
+ + "table { border: 1px solid grey; cellpadding: 5%; width: 100%; table-layout: fixed; }\n"
+ + "th.numeric { width: 4em; }"
+ + "td.pass-number{ text-align: right; color: green; }\n"
+ + "td.error-number{ text-align: right; color: red; }\n"
+ + "td.warning-number{ text-align: right; color: darkorange; }\n"
+ "td.text{ text-align: left; }\n" + "th { font-weight: bold; }\n"
- + "td, th { border: 1px solid grey; }\n"
+ + "td, th { border: 1px solid grey; overflow: hidden; text-overflow: ellipsis; }\n"
+ ".hlRow { background: #EEEEEE; }\n"
+ ".regRow { background: #FFFFFF; }\n";
@@ -73,78 +79,105 @@ public class Reporter {
public Reporter(List<Result> results) {
this._byURLMap = sortByURL(results);
this._byRuleMap = sortByRule(results);
- this._passFailMap = computeSummaryStats(results);
+ this._statsByUrl = computeSummaryStatsByUrl(results);
+ this._statsByRule = computeSummaryStatsByRule(results);
this._ruleNameToDesc = extractRuleDesc(results);
}
/**
* Build the HTML markup for a summary page based on the precomputed map
- * this._passFailMap.
+ * this._statsByUrl.
*
* @return String containing human-readable HTML representing a summary page
*/
public String getSummary() {
StringWriter summaryPage = new StringWriter();
- final Map<String, int[]> scopedMap = this._passFailMap;
+ final Map<String, int[]> scopedMapByUrl = this._statsByUrl;
+ final Map<String, int[]> scopedMapByRule = this._statsByRule;
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")
+ ul();
+ li().a().href("byURL.html")
+ .text("Results organized by URL")
.end().end();
- li().a().href("byRule.html").text("Results organized by Rule")
+ li().a().href("byRule.html")
+ .text("Results organized by Rule")
.end().end();
- end();
+ end();
end();
p();
- div().id("stats");
- makeSummaryStats(scopedMap);
+ div().id("stats");
+ makeSummaryStatsByUrl(scopedMapByUrl);
+ end();
end();
+ p();
+ div().id("stats");
+ makeSummaryStatsByRule(scopedMapByRule);
+ end();
end();
endAll();
done();
}
+ Html makeSummaryStatsByUrl(Map<String, int[]> stats) {
+ return makeSummaryStats("Unique URLs", "byURL.html", stats);
+ }
+
+ Html makeSummaryStatsByRule(Map<String, int[]> stats) {
+ return makeSummaryStats("Rules", "byRule.html", stats);
+ }
+
/**
* 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;
+ Html makeSummaryStats(String title, String detailUrl, Map<String, int[]> stats) {
+ int keycount = stats.size();
+ int[] statsList;
p();
- h3().text("Unique URLs: ");
- span().classAttr("number").text(String.valueOf(uniqueURLs))
+ h3().text(title +": ");
+ span().classAttr("number")
+ .text(String.valueOf(keycount))
.end();
end().end();
p();
- table().id("stats-table");
- tr();
- th().text("URL").end();
- th().text("Pass").end();
- th().text("Fail").end();
- end();
+ table().id("stats-table");
+ thead();
+ tr();
+ th() .text("URL") .end();
+ th().classAttr("numeric").text("Pass") .end();
+ th().classAttr("numeric").text("Error") .end();
+ th().classAttr("numeric").text("Warning").end();
+ end();
+ end();
+ tbody();
int i = 0; // index for **alternate row highlighting**
List<String> sortedKeys = Lists.newArrayList();
- sortedKeys.addAll(passFailMap.keySet());
+ sortedKeys.addAll(stats.keySet());
Collections.sort(sortedKeys);
for (String key : sortedKeys) {
- passFailList = passFailMap.get(key);
+ statsList = stats.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();
+ td().classAttr("text")
+ .a().href(detailUrl+"#"+encode(key)).text(key).end()
+ .end();
+ td().classAttr("pass-number")
+ .text(String.valueOf(statsList[0])).end();
+ td().classAttr("error-number")
+ .text(String.valueOf(statsList[1])).end();
+ td().classAttr("warning-number")
+ .text(String.valueOf(statsList[2])).end();
end();
i++;
}
+ end(); // end <tbody>
end(); // end <table>
return end(); // end <p>
}
@@ -171,8 +204,9 @@ public class Reporter {
sortedKeys.addAll(scopedMap.keySet());
Collections.sort(sortedKeys);
for (String url : sortedKeys) {
- li().h2().a().href(url).text(url).end().end();
- ul();
+ li().id(formatId(url))
+ .h2().a().href(url).text(url).end().end();
+ ul();
int i = 0;
for (Result r : scopedMap.get(url)) {
String cAttr = i % 2 == 0 ? "hlRow" : "regRow";
@@ -215,7 +249,7 @@ public class Reporter {
sortedKeys.addAll(scopedMap.keySet());
Collections.sort(sortedKeys);
for (String rule : sortedKeys) {
- li();
+ li().id(formatId(rule));
b().text(rule).end()
.text(": " + scopedRuleNameToDesc.get(rule));
ul();
@@ -377,44 +411,104 @@ public class Reporter {
/**
* 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 by URL.
+ */
+ private Map<String, int[]> computeSummaryStatsByUrl(List<Result> results) {
+ return computeSummaryStats(results, new Function<Result, String>() {
+ public String apply(Result result) {
+ return result.getURL();
+ }
+ });
+ }
+
+ /**
+ * Compute summary statistics from the results list. This includes number of
+ * passes and fails for each rule checked.
*
* @param results
* a list of results
- * @return a map representing the results sorted by rule name.
+ * @return a map representing the results by URL.
*/
- 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;
+ private Map<String, int[]> computeSummaryStatsByRule(List<Result> results) {
+ return computeSummaryStats(results, new Function<Result, String>() {
+ public String apply(Result result) {
+ return result.getRuleName();
+ }
+ });
+ }
+
+ /**
+ * Compute summary statistics from the results list.
+ *
+ * @param results
+ * a list of results
+ * @param getKey
+ * a function that maps a result to a map key
+ * @return a map representing the results counted according to an
+ * arbitrary result property
+ */
+ private Map<String, int[]> computeSummaryStats(
+ List<Result> results,
+ Function<Result, String> getKey) {
+ /** statsMap semantics: Map<url, {#pass, #error, #warn}> */
+ Map<String, int[]> statsMap = new HashMap<String, int[]>();
+ String key;
+ int pass, error, warn;
+ int[] statsList;
for (Result result : results) {
- pass = fail = 0;
- url = result.getURL();
+ pass = error = warn = 0;
+ key = getKey.apply(result);
if (result.getType() == ResType.Pass) {
- // now we have to parse out how many tests passed
- matcher = numberPassedPattern.matcher(result.getMsg());
- if (matcher.find())
- pass = Integer.parseInt(matcher.group(1)); // throws
- // exception if
- // parse fails
- } else if (result.getType() == ResType.Error) {
+ pass = numberPassed(result);
+ }
+ else if (result.getType() == ResType.Error) {
// each error result corresponds to one test
- fail = 1;
+ error = 1;
+ }
+ else if (result.getType() == ResType.Warning) {
+ warn = 1;
}
- if (passFailMap.containsKey(url)) {
- passFailList = passFailMap.get(url);
+ if (statsMap.containsKey(key)) {
+ statsList = statsMap.get(key);
} else {
- passFailList = new int[] { 0, 0 };
- passFailMap.put(url, passFailList);
+ statsList = new int[] { 0, 0, 0 };
+ statsMap.put(key, statsList);
}
- passFailList[0] += pass;
- passFailList[1] += fail;
+ statsList[0] += pass;
+ statsList[1] += error;
+ statsList[2] += warn;
+ }
+ return statsMap;
+ }
+
+ private Pattern numberPassedPattern = Pattern.compile("passed ([0-9]+) tests");
+
+ private int numberPassed(Result result) {
+ int pass = 0;
+ Matcher m = numberPassedPattern.matcher(result.getMsg());
+ if (m.find()) {
+ // parseInt throws exception if parse fails
+ pass = Integer.parseInt(m.group(1));
+ }
+ return pass;
+ }
+
+ private String formatId(String label) {
+ // java.net.URLEncoder.encode encodes spaces as "+" instead of "%20".
+ return label.replaceAll("\\s", "+");
+ }
+
+ private String encode(String in) {
+ try {
+ return URLEncoder.encode(in, "utf-8");
+ }
+ catch(UnsupportedEncodingException e) {
+ return "";
}
- return passFailMap;
}
}