From 1b35159cf2ff8d86b037f7d442f7b1f4c8e3b2f3 Mon Sep 17 00:00:00 2001 From: Chris Jaekl Date: Sun, 4 Oct 2015 22:06:50 +0900 Subject: [PATCH] Add ability to output HTML report of differences found between two versions. --- prod/cfb.properties | 11 ++ prod/net/jaekl/cfb/CFB.java | 29 ++-- prod/net/jaekl/cfb/CfbBundle.java | 26 ++- prod/net/jaekl/cfb/analyze/Analyzer.java | 6 +- prod/net/jaekl/cfb/analyze/Delta.java | 10 ++ prod/net/jaekl/cfb/analyze/HtmlReport.java | 181 +++++++++++++++++++++ prod/net/jaekl/cfb/store/Run.java | 26 +++ 7 files changed, 273 insertions(+), 16 deletions(-) create mode 100644 prod/net/jaekl/cfb/analyze/HtmlReport.java create mode 100644 prod/net/jaekl/cfb/store/Run.java diff --git a/prod/cfb.properties b/prod/cfb.properties index 5027818..e56f819 100644 --- a/prod/cfb.properties +++ b/prod/cfb.properties @@ -1,5 +1,16 @@ analysis.failed=Attempt to analyze source code failed. Will now stop. +analyzed.at=Analyzed at {0} cannot.connect.to.db=Unable to connect to, or to initialize, database {2} on {0}:{1} as user {3}. cannot.exec=Got result code {1} when attempting to execute command-line: {0} +cfb=Comparative FindBugs +cfb.report=Comparative FindBugs Report +fixed.bugs=Fixed Bugs +new.bugs=New Bugs +new.version=New Version +num.bugs={0} +num.bugs.old={0} (common to both versions) +old.bugs=Old Bugs +old.version=Old Version stderr.was=-----8<------ Error (stderr) output was: ------8<----- stdout.was=-----8<----- Console (stdout) output was: -----8<----- +version.num={1} (analyzed at {0}) diff --git a/prod/net/jaekl/cfb/CFB.java b/prod/net/jaekl/cfb/CFB.java index d202d6f..94ef08f 100644 --- a/prod/net/jaekl/cfb/CFB.java +++ b/prod/net/jaekl/cfb/CFB.java @@ -19,6 +19,7 @@ import java.util.Locale.Category; import net.jaekl.cfb.analyze.Analysis; import net.jaekl.cfb.analyze.Analyzer; import net.jaekl.cfb.analyze.Delta; +import net.jaekl.cfb.analyze.HtmlReport; import net.jaekl.cfb.analyze.MessageMap; import net.jaekl.cfb.db.CfbSchema; import net.jaekl.cfb.db.TypeMismatchException; @@ -50,6 +51,7 @@ public class CFB { String m_pass; // db password String m_buildNum; // build number (version) boolean m_removeSchema; // purge DB schema + File m_output; // File to which we should write our output (report) CFB(Locale locale) { m_driver = new PostgresqlDriver(); @@ -66,19 +68,21 @@ public class CFB { m_user = "user"; m_buildNum = null; m_removeSchema = false; + m_output = null; } Options createOptions() { Options opt = new Options(); - opt.addOption("d", "dbname", true, "DB name"); - opt.addOption("f", "fbp", true, "FindBugsProject file"); - opt.addOption("h", "host", true, "DB hostname"); - opt.addOption("n", "number", true, "Build number (version)"); - opt.addOption("p", "pass", true, "DB password"); - opt.addOption("r", "remove", false, "Remove database schema (drop all data)"); - opt.addOption("t", "port", true, "DB port"); - opt.addOption("u", "user", true, "DB username"); + opt.addOption("d", "dbname", true, "DB name"); + opt.addOption(null, "drop-tables", false, "Remove database schema (drop all data)"); + opt.addOption("f", "fbp", true, "FindBugsProject file"); + opt.addOption("h", "host", true, "DB hostname"); + opt.addOption("n", "number", true, "Build number (version)"); + opt.addOption("o", "outfile", true, "Output report filename"); + opt.addOption("p", "pass", true, "DB password"); + opt.addOption("t", "port", true, "DB port"); + opt.addOption("u", "user", true, "DB username"); return opt; } @@ -100,10 +104,13 @@ public class CFB { if (line.hasOption("n")) { m_buildNum = line.getOptionValue("n"); } + if (line.hasOption("o")) { + m_output = new File(line.getOptionValue("o")); + } if (line.hasOption("p")) { m_pass = line.getOptionValue("p"); } - m_removeSchema = line.hasOption("r"); + m_removeSchema = line.hasOption("drop-tables"); if (line.hasOption("t")) { m_port = Integer.parseInt(line.getOptionValue("t")); } @@ -192,7 +199,9 @@ public class CFB { store.put(analysis); Analysis prior = store.getPrior(analysis); Delta delta = new Delta(prior, analysis); - delta.dump(pw); + + HtmlReport report = new HtmlReport(m_bundle, messageMap.getColl()); + report.write(m_output, delta); } catch (SQLException exc) { reportUnableToConnect(pw, exc); diff --git a/prod/net/jaekl/cfb/CfbBundle.java b/prod/net/jaekl/cfb/CfbBundle.java index 335a68d..8ea3c68 100644 --- a/prod/net/jaekl/cfb/CfbBundle.java +++ b/prod/net/jaekl/cfb/CfbBundle.java @@ -2,6 +2,7 @@ package net.jaekl.cfb; // Copyright (C) 2015 Christian Jaekl +import java.text.MessageFormat; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; @@ -11,10 +12,23 @@ import net.jaekl.qd.QDBundleFactory; public class CfbBundle { public static final String ANALYSIS_FAILED = "analysis.failed"; + public static final String ANALYZED_AT = "analyzed.at"; public static final String CANNOT_CONNECT = "cannot.connect.to.db"; public static final String CANNOT_EXEC = "cannot.exec"; + public static final String CFB = "cfb"; + public static final String CFB_REPORT = "cfb.report"; + public static final String COMPARING_RUNS = "comparing.runs"; + public static final String COMPARING_VERSIONS = "comparing.versions"; + public static final String FIXED_BUGS = "fixed.bugs"; + public static final String NEW_BUGS = "new.bugs"; + public static final String NEW_VERSION = "new.version"; + public static final String NUM_BUGS = "num.bugs"; + public static final String NUM_BUGS_OLD = "num.bugs.old"; + public static final String OLD_BUGS = "old.bugs"; + public static final String OLD_VERSION = "old.version"; public static final String STDERR_WAS = "stderr.was"; public static final String STDOUT_WAS = "stdout.was"; + public static final String VERSION_NUM = "version.num"; final static String BUNDLE_NAME = "cfb"; @@ -40,10 +54,11 @@ public class CfbBundle { m_bundle = QDBundleFactory.getInst().getBundle(BUNDLE_NAME, locale); } - public String get(String key) { + public String get(String key, Object... arguments) { try { if (null != m_bundle) { - return m_bundle.getString(key); + String pattern = m_bundle.getString(key); + return MessageFormat.format(pattern, arguments); } } catch (MissingResourceException exc) { @@ -51,6 +66,11 @@ public class CfbBundle { exc.printStackTrace(); // Fall through to the fallback behaviour below } - return "[" + key + "]"; + + StringBuilder sb = new StringBuilder("[" + key + "]"); + for (Object obj : arguments) { + sb.append("[" + obj + "]"); + } + return sb.toString(); } } diff --git a/prod/net/jaekl/cfb/analyze/Analyzer.java b/prod/net/jaekl/cfb/analyze/Analyzer.java index 3f3c4cf..a7903ee 100644 --- a/prod/net/jaekl/cfb/analyze/Analyzer.java +++ b/prod/net/jaekl/cfb/analyze/Analyzer.java @@ -10,13 +10,13 @@ import java.util.Date; import java.util.Locale; import java.util.Locale.Category; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - import net.jaekl.cfb.CfbBundle; import net.jaekl.cfb.util.Command; import net.jaekl.qd.xml.XmlParseException; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + public class Analyzer { MessageMap m_msgMap; diff --git a/prod/net/jaekl/cfb/analyze/Delta.java b/prod/net/jaekl/cfb/analyze/Delta.java index 51971e0..865b5fc 100644 --- a/prod/net/jaekl/cfb/analyze/Delta.java +++ b/prod/net/jaekl/cfb/analyze/Delta.java @@ -3,17 +3,24 @@ package net.jaekl.cfb.analyze; import java.io.PrintWriter; import java.util.HashSet; +import net.jaekl.cfb.store.Run; import net.jaekl.cfb.xml.BugInstance; // Compute and store the delta (difference) between two analyses public class Delta { + Run m_earlier; + Run m_later; + HashSet m_fixed; // bugs that have been fixed HashSet m_common; // bugs that are present in both versions HashSet m_new; // bugs introduced in the new version public Delta(Analysis before, Analysis after) { + m_earlier = new Run(before); + m_later = new Run(after); + m_fixed = new HashSet(); m_common = new HashSet(); m_new = new HashSet(); @@ -30,6 +37,9 @@ public class Delta { public BugInstance[] getNew() { return m_new.toArray(new BugInstance[m_new.size()]); } public int getNumNew() { return m_new.size(); } + public Run getEarlier() { return m_earlier; } + public Run getLater() { return m_later; } + public void dump(PrintWriter pw) { pw.println("========================="); pw.println(" NEW BUGS (" + m_new.size() + ")"); diff --git a/prod/net/jaekl/cfb/analyze/HtmlReport.java b/prod/net/jaekl/cfb/analyze/HtmlReport.java new file mode 100644 index 0000000..4482f0a --- /dev/null +++ b/prod/net/jaekl/cfb/analyze/HtmlReport.java @@ -0,0 +1,181 @@ +package net.jaekl.cfb.analyze; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintWriter; + +import net.jaekl.cfb.CfbBundle; +import net.jaekl.cfb.store.Location; +import net.jaekl.cfb.xml.BugInstance; +import net.jaekl.cfb.xml.messages.BugPattern; +import net.jaekl.cfb.xml.messages.MessageCollection; + +public class HtmlReport { + CfbBundle m_bundle; + MessageCollection m_msgColl; + + public HtmlReport(CfbBundle bundle, MessageCollection msgColl) + { + m_bundle = bundle; + m_msgColl = msgColl; + } + + public void write(File output, Delta delta) throws IOException + { + try ( FileOutputStream fos = new FileOutputStream(output); + PrintWriter pw = new PrintWriter(fos); ) + { + write(pw, delta); + } + } + + void write(PrintWriter pw, Delta delta) + { + startPage(pw, delta); + } + + void startPage(PrintWriter pw, Delta delta) + { + writeHeader(pw, delta); + pw.println(" "); + writeBody(pw, delta); + pw.println(" "); + pw.println(""); + } + + String trans(String key, Object... arguments) { + return m_bundle.get(key, arguments); + } + + void writeBody(PrintWriter pw, Delta delta) + { + writeSummary(pw, delta); + writeBugs(pw, CfbBundle.NEW_BUGS, delta.getNew()); + writeBugs(pw, CfbBundle.FIXED_BUGS, delta.getFixed()); + writeBugs(pw, CfbBundle.OLD_BUGS, delta.getCommon()); + } + + void writeBugLocations(PrintWriter pw, BugInstance bug) + { + for (Location loc : bug.getLocations()) { + StringBuffer sb = new StringBuffer(); + + if (null != loc) { + if (null != loc.getClassName()) { + sb.append(loc.getClassName()); + } + if ((null != loc.getMethodName()) && (loc.getMethodName().length() > 0)) { + sb.append(".").append(loc.getMethodName()).append("()"); + } + int start = loc.getStart(); + int end = loc.getEnd(); + if (start > 0) { + sb.append(":").append("" + start); + if ((end > 0) && (end > start)) { + sb.append("-").append("" + end); + } + } + + if (null != loc.getMethodRole()) { + sb.append(" (" + loc.getMethodRole() + ")"); + } + } + + pw.write(" "); + pw.write(" " + sb.toString() + ""); + pw.write(" "); + } + } + + void writeBugs(PrintWriter pw, String key, BugInstance[] bugs) + { + if (null == bugs || bugs.length < 1) { + return; + } + + writeSectionHeading(pw, trans(key)); + + for (BugInstance bug : bugs) { + BugPattern pattern = m_msgColl.getPattern(bug.getType()); + + pw.write("

"); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write(" "); + writeBugLocations(pw, bug); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write(" "); + pw.write("
" + bug.getCategory() + "" + bug.getType() + "
" + pattern.getShort() + "
" + pattern.getDetails() + "
"); + pw.write("

"); + } + } + + void writeHeader(PrintWriter pw, Delta delta) + { + String title = trans(CfbBundle.CFB_REPORT); + + pw.println(""); + pw.println(" "); + pw.println(" "); + pw.println(" " + title + ""); + writeStyle(pw); + pw.println(" "); + } + + void writeSectionHeading(PrintWriter pw, String heading) + { + pw.println("

"); + pw.println("
" + heading + "
"); + pw.println("

"); + } + + void writeSummary(PrintWriter pw, Delta delta) + { + final String SEP = ":  "; + + writeSectionHeading(pw, trans(CfbBundle.CFB_REPORT)); + pw.println("

"); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println(" "); + pw.println("
" + trans(CfbBundle.NEW_VERSION) + SEP + "" + delta.getLater().constructVersionText(m_bundle) + "
" + trans(CfbBundle.OLD_VERSION) + SEP + "" + delta.getEarlier().constructVersionText(m_bundle) + "
 
" + trans(CfbBundle.NEW_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS, delta.getNumNew()) + "
" + trans(CfbBundle.FIXED_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS, delta.getNumFixed()) + "
" + trans(CfbBundle.OLD_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS_OLD, delta.getNumCommon()) + "
"); + pw.println("

"); + } + + void writeStyle(PrintWriter pw) + { + pw.println(" "); + } +} diff --git a/prod/net/jaekl/cfb/store/Run.java b/prod/net/jaekl/cfb/store/Run.java new file mode 100644 index 0000000..5fa5104 --- /dev/null +++ b/prod/net/jaekl/cfb/store/Run.java @@ -0,0 +1,26 @@ +package net.jaekl.cfb.store; + +import net.jaekl.cfb.CfbBundle; +import net.jaekl.cfb.analyze.Analysis; + +public class Run { + java.util.Date m_analysisStart; + String m_buildNumber; + + public Run(Analysis analysis) + { + m_analysisStart = analysis.getStart(); + m_buildNumber = analysis.getBuildNumber(); + } + + public java.util.Date getAnalysisStart() { return new java.util.Date(m_analysisStart.getTime()); } + public String getBuildNumber() { return m_buildNumber; } + + public String constructVersionText(CfbBundle bundle) + { + if (null == m_buildNumber) { + return bundle.get(CfbBundle.ANALYZED_AT, m_analysisStart); + } + return bundle.get(CfbBundle.VERSION_NUM, m_analysisStart, m_buildNumber); + } +} -- 2.30.2