diff options
author | Benjamin Jones <bjones@galois.com> | 2012-12-07 15:58:32 -0800 |
---|---|---|
committer | Benjamin Jones <bjones@galois.com> | 2012-12-07 15:58:32 -0800 |
commit | f6e2a45110f0435d9c41ad84477aa03736b24fe8 (patch) | |
tree | ee54f4b2d231f30c7274fe03b7fb200b70e9d8c5 /headless | |
parent | 6a4f5fbca4137e4aa7bbcaff0bb0c6d07ef7a8a9 (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.java | 335 | ||||
-rw-r--r-- | headless/src/test/java/com/galois/fiveui/ReporterTest.java | 27 |
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); + } } |