Add ability to output HTML report of differences found between two versions.
authorChris Jaekl <cejaekl@yahoo.com>
Sun, 4 Oct 2015 13:06:50 +0000 (22:06 +0900)
committerChris Jaekl <cejaekl@yahoo.com>
Sun, 4 Oct 2015 13:06:50 +0000 (22:06 +0900)
prod/cfb.properties
prod/net/jaekl/cfb/CFB.java
prod/net/jaekl/cfb/CfbBundle.java
prod/net/jaekl/cfb/analyze/Analyzer.java
prod/net/jaekl/cfb/analyze/Delta.java
prod/net/jaekl/cfb/analyze/HtmlReport.java [new file with mode: 0644]
prod/net/jaekl/cfb/store/Run.java [new file with mode: 0644]

index 50278187010526e786517f685c6b6a9c26e2f378..e56f819a60ed295b9af6ed37fae1cd5834f94c81 100644 (file)
@@ -1,5 +1,16 @@
 analysis.failed=Attempt to analyze source code failed.  Will now stop.
 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}
 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<-----
 stderr.was=-----8<------ Error (stderr) output was: ------8<-----
 stdout.was=-----8<----- Console (stdout) output was: -----8<-----
+version.num={1} (analyzed at {0})
index d202d6fcb518b1ee54697588fc6c0204fd92e4b7..94ef08f8a996cecf7ce681574b95bc3fd1ad62f9 100644 (file)
@@ -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.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;
 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
        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();
        
        CFB(Locale locale) {
                m_driver = new PostgresqlDriver();
@@ -66,19 +68,21 @@ public class CFB {
                m_user = "user";
                m_buildNum = null;
                m_removeSchema = false;
                m_user = "user";
                m_buildNum = null;
                m_removeSchema = false;
+               m_output = null;
        }
        
        Options createOptions() {
                Options opt = new Options();
                
        }
        
        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;
        }
                
                return opt;
        }
@@ -100,10 +104,13 @@ public class CFB {
                        if (line.hasOption("n")) {
                                m_buildNum = line.getOptionValue("n");
                        }
                        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");
                        }
                        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"));
                        }
                        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);
                        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);
                }
                catch (SQLException exc) {
                        reportUnableToConnect(pw, exc);
index 335a68dfbbb761c914b5523fee10115bba5e75e9..8ea3c68f669d8306f6e43027251f47aa22e46ed8 100644 (file)
@@ -2,6 +2,7 @@ package net.jaekl.cfb;
 
 // Copyright (C) 2015 Christian Jaekl
 
 
 // Copyright (C) 2015 Christian Jaekl
 
+import java.text.MessageFormat;
 import java.util.Locale;
 import java.util.MissingResourceException;
 import java.util.ResourceBundle;
 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 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 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 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";
        
        
        final static String BUNDLE_NAME = "cfb";
        
@@ -40,10 +54,11 @@ public class CfbBundle {
                m_bundle = QDBundleFactory.getInst().getBundle(BUNDLE_NAME, locale); 
        }
        
                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) {
                try {
                        if (null != m_bundle) {
-                               return m_bundle.getString(key);
+                               String pattern = m_bundle.getString(key);
+                               return MessageFormat.format(pattern, arguments);
                        }
                }
                catch (MissingResourceException exc) {
                        }
                }
                catch (MissingResourceException exc) {
@@ -51,6 +66,11 @@ public class CfbBundle {
                        exc.printStackTrace();  
                        // Fall through to the fallback behaviour below
                }
                        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();
        }
 }
        }
 }
index 3f3c4cfa1395c548f2066486d737f06f9637f9fd..a7903ee3d3ef48c1f7173037b6ecd08e9fb8900c 100644 (file)
@@ -10,13 +10,13 @@ import java.util.Date;
 import java.util.Locale;
 import java.util.Locale.Category;
 
 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 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;
        
 public class Analyzer {
        MessageMap m_msgMap;
        
index 51971e06c7589a01336a4bfce8b8ba0785f9a04b..865b5fce57fdbf7c9b21807616f4132ffd226752 100644 (file)
@@ -3,17 +3,24 @@ package net.jaekl.cfb.analyze;
 import java.io.PrintWriter;
 import java.util.HashSet;
 
 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 {
 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<BugInstance> m_fixed;           // bugs that have been fixed
        HashSet<BugInstance> m_common;  // bugs that are present in both versions
        HashSet<BugInstance> m_new;             // bugs introduced in the new version
        
        public Delta(Analysis before, Analysis after)
        {
        HashSet<BugInstance> m_fixed;           // bugs that have been fixed
        HashSet<BugInstance> m_common;  // bugs that are present in both versions
        HashSet<BugInstance> 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<BugInstance>();
                m_common = new HashSet<BugInstance>();
                m_new = new HashSet<BugInstance>();
                m_fixed = new HashSet<BugInstance>();
                m_common = new HashSet<BugInstance>();
                m_new = new HashSet<BugInstance>();
@@ -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 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() + ")");
        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 (file)
index 0000000..4482f0a
--- /dev/null
@@ -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("  <BODY>");
+               writeBody(pw, delta);
+               pw.println("  </BODY>");
+               pw.println("</HTML>");
+       }
+       
+       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("        <TR>");
+                       pw.write("          <TD COLSPAN=\"2\" CLASS=\"Loc\">" + sb.toString() + "</TD>");
+                       pw.write("        </TR>");                                      
+               }
+       }
+       
+       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("    <P>");
+                       pw.write("      <TABLE CLASS=\"bug\">");
+                       pw.write("        <TR>");
+                       pw.write("          <TD WIDTH=\"20%\">" + bug.getCategory() + "</TD>");
+                       pw.write("          <TD>" + bug.getType() + "</TD>");
+                       pw.write("        </TR>");
+                       writeBugLocations(pw, bug);
+                       pw.write("        <TR>");
+                       pw.write("          <TD COLSPAN=\"2\">" + pattern.getShort() + "</TD>");
+                       pw.write("        </TR>");
+                       pw.write("        <TR>");
+                       pw.write("          <TD COLSPAN=\"2\">" + pattern.getDetails() + "</TD>");
+                       pw.write("        </TR>");
+                       pw.write("      </TABLE>");
+                       pw.write("    </P>");
+               }
+       }
+       
+       void writeHeader(PrintWriter pw, Delta delta)
+       {
+               String title = trans(CfbBundle.CFB_REPORT);
+               
+               pw.println("<HTML>");
+               pw.println("  <HEAD>");
+               pw.println("    <META CHARSET=\"UTF-8\"/>");
+               pw.println("    <TITLE>" + title + "</TITLE>");
+               writeStyle(pw);
+               pw.println("  </HEAD>");
+       }
+       
+       void writeSectionHeading(PrintWriter pw, String heading)
+       {
+               pw.println("    <P CLASS=\"SectionHead\">");
+               pw.println("      <TABLE WIDTH=\"100%\"><TR><TD>" + heading + "</TD></TR></TABLE>");
+               pw.println("    </P>");
+       }
+       
+       void writeSummary(PrintWriter pw, Delta delta)
+       {
+               final String SEP = ":&nbsp;&nbsp;";
+               
+               writeSectionHeading(pw, trans(CfbBundle.CFB_REPORT));
+               pw.println("    <P>");
+               pw.println("      <TABLE>");
+               pw.println("        <TR>");
+               pw.println("          <TD CLASS=\"CategoryName\">" + trans(CfbBundle.NEW_VERSION) + SEP + "</TD>");
+               pw.println("          <TD CLASS=\"CategoryValue\">" + delta.getLater().constructVersionText(m_bundle) + "</TD>");
+               pw.println("        </TR>");
+               pw.println("        <TR>");
+               pw.println("          <TD CLASS=\"CategoryName\">" + trans(CfbBundle.OLD_VERSION) + SEP + "</TD>");
+               pw.println("          <TD CLASS=\"CategoryValue\">" + delta.getEarlier().constructVersionText(m_bundle) + "</TD>");
+               pw.println("        </TR>");
+               pw.println("        <TR><TD>&nbsp;</TD></TR>");
+               pw.println("        <TR>");
+               pw.println("          <TD CLASS=\"CategoryName\">" + trans(CfbBundle.NEW_BUGS) + SEP + "</TD>");
+               pw.println("          <TD CLASS=\"CategoryValue\">" + trans(CfbBundle.NUM_BUGS, delta.getNumNew()) + "</TD>");
+               pw.println("        </TR>");
+               pw.println("        <TR>");
+               pw.println("          <TD CLASS=\"CategoryName\">" + trans(CfbBundle.FIXED_BUGS) + SEP + "</TD>");
+               pw.println("          <TD CLASS=\"CategoryValue\">" + trans(CfbBundle.NUM_BUGS, delta.getNumFixed()) + "</TD>");
+               pw.println("        </TR>");
+               pw.println("        <TR>");
+               pw.println("          <TD CLASS=\"CategoryName\">" + trans(CfbBundle.OLD_BUGS) + SEP + "</TD>");
+               pw.println("          <TD CLASS=\"CategoryValue\">" + trans(CfbBundle.NUM_BUGS_OLD, delta.getNumCommon()) + "</TD>");
+               pw.println("        </TR>");
+               pw.println("      </TABLE>");
+               pw.println("    </P>");
+       }
+       
+       void writeStyle(PrintWriter pw)
+       {
+               pw.println("    <STYLE>");
+               pw.println("                   body { background-color: #F0F0FF; }");
+           pw.println("    .CategoryName  { text-align: right; }");
+           pw.println("    .CategoryValue { text-align: left; }");
+               pw.println("    .Loc           { font-family: monospace; }");
+           pw.println("    .SectionHead   td { background-color: #0000FF; color: #FFFFFF; font-size: 1.25em; font-weight: bold; }");
+               pw.println("    </STYLE>");
+       }
+}
diff --git a/prod/net/jaekl/cfb/store/Run.java b/prod/net/jaekl/cfb/store/Run.java
new file mode 100644 (file)
index 0000000..5fa5104
--- /dev/null
@@ -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);
+       }
+}