Add a web frontend to the warning database: cherrypy+genshi FTW!
authorBenjamin Smedberg <benjamin@smedbergs.us>
Fri, 19 Dec 2008 16:50:47 -0500
changeset 8 c33d0e330649
parent 7 c77013199a86
child 9 1e34d76ac686
push id4
push userbsmedberg@mozilla.com
push dateFri, 19 Dec 2008 22:03:11 +0000
Add a web frontend to the warning database: cherrypy+genshi FTW!
warning-ui/build.html
warning-ui/common.inc
warning-ui/index.html
warning-ui/search.html
warning-ui/static/warnings.css
warning-ui/ui.py
warning-ui/warning.html
new file mode 100644
--- /dev/null
+++ b/warning-ui/build.html
@@ -0,0 +1,54 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="common.inc" />
+
+  <head>
+    <title>Warning for build ${id}</title>
+  </head>
+  <body class="build">
+    <h1>Build ${id}</h1>
+
+    <div class="h2block">
+      <h2>Revision:</h2>
+      <a href="http://hg.mozilla.org/mozilla-central/pushloghtml?changeset=${rev}">${rev}</a>
+    </div>
+    <div class="h2block" py:if="previd is not None and prevrev != rev">
+      <h2>Changes since <a href="/build?id=${previd}">last build</a>:</h2>
+      <a href="http://hg.mozilla.org/mozilla-central/pushloghtml?fromchange=${prevrev}&amp;tochange=${rev}">Log</a>
+    </div>
+    <div class="h2block">
+      <h2>Unique warnings:</h2> ${unique}
+    </div>
+    <div class="h2block">
+      <h2>Total warnings:</h2> ${total}
+    </div>
+
+    <py:if test="previd is not None">
+      <div py:if="len(newwarnings)">
+        <h3>New:</h3>
+
+        <ul>
+          <li py:for="signature, file, lineno, msg in newwarnings">
+            <a href="${genlink('warning', signature=signature)}">${file}:${lineno} - ${msg}</a>
+          </li>
+        </ul>
+      </div>
+
+      <div py:if="len(fixedwarnings)">
+        <h3>Fixed:</h3>
+
+        <ul>
+          <li py:for="signature, file, lineno, msg in fixedwarnings">
+            <a href="${genlink('warning', signature=signature)}">${file}:${lineno} - ${msg}</a>
+          </li>
+        </ul>
+      </div>
+    </py:if>
+
+    <h2>Search warnings:</h2>
+
+    ${searchform('', '', 'unused variable%')}
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/warning-ui/common.inc
@@ -0,0 +1,28 @@
+<!-- -*- Mode: XML -*- -->
+<?python from urllib import quote, urlencode ?>
+<div xmlns="http://www.w3.org/1999/xhtml"
+     xmlns:py="http://genshi.edgewall.org/"
+     py:strip="True">
+  <py:def function="searchform(user, path, msg)">
+    <form action="/search">
+      User LIKE: <input type="text" name="user" size="25" value="${user}"/><br />
+      Directory LIKE: <input type="text" name="path" size="25" value="${path}"/><br />
+      Warning message LIKE: <input type="text" name="msg" size="30" value="${msg}"/><br />
+      <input type="hidden" name="id" value="${id}" />
+      <input type="submit" value="Search" />
+    </form>
+  </py:def>
+
+  <py:def function="genlink(path, **params)">/${quote(path)}<py:if test="len(params)">?${urlencode(params)}</py:if></py:def>
+
+  <head py:match="head">
+    ${select("*|text()")}
+    <link type="text/css" rel="stylesheet" href="/static/warnings.css" />
+  </head>
+  <body py:match="body">
+    <div class="header">
+      <a href="/">Compiler Warnings Tracker: mozilla-central</a>
+    </div>
+    ${select("*|text()")}
+  </body>
+</div>
new file mode 100644
--- /dev/null
+++ b/warning-ui/index.html
@@ -0,0 +1,107 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="common.inc" />
+
+  <head>
+    <title>Warnings</title>
+  </head>
+  <body class="index">
+    <svg xmlns="http://www.w3.org/2000/svg" id="graph" preserveAspectRatio="none"
+         width="500" height="200" style="border: 1px solid black;" />
+    
+    <ul>
+      <li py:for="buildnumber, rev, unique in builds"><a href="build?id=${buildnumber}">${rev}</a></li>
+    </ul>
+
+    <script type="application/x-javascript">
+      var kWidth, kHeight, min, max, g, i, x, y, s, points, txt, a, hit, gline;
+
+      kWidth = 500;
+      kHeight = 200;
+
+      gData = [
+      <py:for each="buildnumber, rev, unique in builds">
+        [${buildnumber}, "${rev}", ${unique}],
+      </py:for>
+      ];
+
+      // Oh, my kingdom for a .reduce, but I want this to work in Safari
+      min = null;
+      max = null;
+      for (i = 0; i &lt; gData.length; ++i) {
+        if (min == null || gData[i][2] &lt; min)
+          min = gData[i][2];
+        if (max == null || gData[i][2] &gt; max)
+          max = gData[i][2];
+      }
+
+      g = document.getElementById("graph");
+
+      for (i = min - min % 25; i &lt; max; i += 25) {
+        y = kHeight - ((i - min) / (max - min) * (kHeight - 10)) - 5;
+        s = document.createElementNS('http://www.w3.org/2000/svg', 'line');
+        s.setAttribute('class', 'scale');
+        s.x1.baseVal.value = 40;
+        s.x2.baseVal.value = 490;
+        s.y1.baseVal.value = y;
+        s.y2.baseVal.value = y;
+        g.appendChild(s);
+
+        s = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+        s.setAttribute('class', 'scaleText');
+        s.textContent = i;
+        s.setAttribute('x', 38);
+        s.setAttribute('y', y);
+        g.appendChild(s);
+      }
+
+      points = [];
+
+      for (i = gData.length - 1; i &gt;= 0; --i) {
+        id = gData[i][0];
+        rev = gData[i][1];
+        t = gData[i][2];
+
+        a = document.createElementNS('http://www.w3.org/2000/svg', 'a');
+        a.setAttributeNS('http://www.w3.org/1999/xlink', 'href', '/build?id=' + id);
+        g.appendChild(a);
+
+        txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+        txt.setAttribute('class', 'overlay');
+        txt.textContent = "Warnings: " + t;
+        txt.setAttribute('x', kWidth / 2);
+        txt.setAttribute('y', 20);
+        a.appendChild(txt);
+
+        txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
+        txt.setAttribute('class', 'overlay');
+        txt.textContent = 'Rev: ' + rev;
+        txt.setAttribute('x', kWidth / 2);
+        txt.setAttribute('y', 40);
+        a.appendChild(txt);
+        
+        x = (gData.length - i) / gData.length * (kWidth - 50) + 40;
+        y = kHeight - ((t - min) / (max - min) * (kHeight - 10)) - 5;
+
+        points.push(x, y);
+
+        if (isNaN(y))
+          throw new Error("isNaN: " + [i, t, x, y, max, min]);
+
+        hit = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
+        hit.setAttribute('class', 'mark');
+        hit.cx.baseVal.value = x;
+        hit.cy.baseVal.value = y;
+        hit.r.baseVal.value = 3;
+        a.appendChild(hit);
+      }
+
+      gline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
+      gline.setAttribute('id', 'gline');
+      gline.setAttribute('points', points.join(' '));
+
+      g.insertBefore(gline, g.childNodes[0]);
+    </script>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/warning-ui/search.html
@@ -0,0 +1,31 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="common.inc" />
+
+  <head>
+    <title>Warning for build ${id}</title>
+  </head>
+  <body>
+    <h1>Warnings in <a href="/build?id=${id}">build ${id}</a></h1>
+
+    <ul>
+      <li py:if="user != ''">User LIKE '${user}'</li>
+      <li py:if="path != ''">Path LIKE '${path}'</li>
+    </ul>
+
+    <h2>Results</h2>
+
+    <p py:if="len(results) == 0">No warnings found!</p>
+
+    <ul py:if="len(results)">
+      <li py:for="signature, file, lineno, msg in results">
+        <a href="${genlink('warning', signature=signature)}">${file}:${lineno} - ${msg}</a>
+      </li>
+    </ul>
+
+    <h2>Search again:</h2>
+    ${searchform(user, path, msg)}
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/warning-ui/static/warnings.css
@@ -0,0 +1,63 @@
+body {
+  font-family: sans-serif;
+}
+
+h1 {
+  font-size: 180%;
+  font-weight: bold;
+}
+
+h2 {
+  font-size: 150%;
+  font-weight: bold;
+}
+
+.h2block {
+  font-size: 150%;
+}
+
+.h2block h2 {
+  display: inline;
+  font-size: 100%;
+}
+
+.mark {
+  fill: rgb(200, 120, 12);
+  fill-opacity: 0.5;
+}
+#gline {
+  stroke: rgb(250, 160, 20);
+  stroke-width: 2px;
+  fill: none;
+}
+.overlay {
+  font-family: sans-serif;
+  font-size: 18px;
+  font-weight: bold;
+  fill: black;
+  fill-opacity: 0.7;
+  display: none;
+  text-anchor: middle;
+}
+a:hover .overlay {
+  display: inline;
+}
+.scale {
+  stroke: #AAA;
+  stroke-width: 1px;
+}
+.scaleText {
+  font-family: sans-serif;
+  font-size: 12px;
+  fill: #666;
+  text-anchor: end;
+  dominant-baseline: middle;
+}
+
+.header {
+  background-color: rgb(250, 160, 20);
+  color: black;
+  font-size: 150%;
+  padding: 5px;
+  margin-bottom: 1em;
+}
new file mode 100644
--- /dev/null
+++ b/warning-ui/ui.py
@@ -0,0 +1,222 @@
+import cherrypy, os, sys, re, sqlite3
+from genshi.template import TemplateLoader
+
+class Root(object):
+    def __init__(self):
+        self.loader = TemplateLoader(cherrypy.config['tools.staticdir.root'],
+                                     auto_reload=True)
+
+    def dbcursor(self):
+        return sqlite3.connect(cherrypy.request.app.config['database']['path']).cursor()
+
+    def render(self, tmpl, **kwargs):
+        cherrypy.response.headers['Content-Type'] = 'application/xhtml+xml'
+        tmpl = self.loader.load(tmpl)
+        return tmpl.generate(**kwargs).render('xhtml', doctype='xhtml')
+
+    @cherrypy.expose
+    def index(self):
+        try:
+            cur = self.dbcursor()
+            cur.execute('''SELECT builds.buildnumber, builds.rev, uwarnings.ucount
+                           FROM builds LEFT OUTER JOIN
+                             (SELECT buildnumber, count(*) AS ucount
+                              FROM warnings
+                              GROUP BY buildnumber) AS uwarnings
+                           ON builds.buildnumber = uwarnings.buildnumber
+                           ORDER BY builds.buildnumber DESC''')
+            return self.render('index.html', builds=cur.fetchall())
+        finally:
+            cur.close()
+
+    @cherrypy.expose
+    def search(self, id, user="", path="", msg=""):
+        id = int(id)
+        user = user.strip()
+        path = path.strip()
+        msg = msg.strip()
+        
+        bindp = [id]
+
+        subclauses = []
+        if user != '':
+            subclauses.append('AND wl2.blamewho LIKE ?')
+            bindp.append(user)
+
+        if path != '':
+            subclauses.append('AND wl2.file LIKE ?')
+            bindp.append(path)
+
+        if msg != '':
+            subclauses.append('AND wl2.msg LIKE ?')
+            bindp.append(msg)
+
+        if len(subclauses):
+            clause = '''AND EXISTS (SELECT *
+                                    FROM wlines AS wl2
+                                    WHERE
+                                      wl2.buildnumber = warnings.buildnumber AND
+                                      wl2.signature = warnings.signature %s)''' % ' '.join(subclauses)
+        else:
+            clause = ''
+
+        try:
+            cur = self.dbcursor()
+            q = '''SELECT warnings.signature, file, lineno, msg
+                   FROM warnings, wlines
+                   WHERE
+                     warnings.buildnumber = wlines.buildnumber AND
+                     warnings.signature = wlines.signature AND
+                     wlines.wline = 0 AND
+                     warnings.buildnumber = ? %s''' % (clause, )
+            print "Query: %s\nBind: %s" % (q, bindp)
+
+            cur.execute(q, bindp)
+
+            return self.render('search.html',
+                               id=id,
+                               user=user,
+                               path=path,
+                               msg=msg,
+                               results=cur.fetchall())
+        finally:
+            cur.close()
+
+    @cherrypy.expose
+    def build(self, id):
+        id = int(id)
+        try:
+            cur = self.dbcursor()
+            cur.execute('''SELECT rev
+                           FROM builds
+                           WHERE buildnumber = ?''', (id,))
+            rev, = cur.fetchone()
+
+            cur.execute('''SELECT count(*) as ucount, sum(count) AS tcount
+                           FROM warnings
+                           WHERE buildnumber = ?''', (id,))
+            unique, total = cur.fetchone()
+
+            cur.execute('''SELECT buildnumber, rev
+                           FROM builds
+                           WHERE
+                             buildnumber < ?
+                           ORDER BY buildnumber DESC
+                           LIMIT 1''', (id,))
+            prev = cur.fetchone()
+            if prev is None:
+                previd = None
+                prevrev = None
+                newwarnings = None
+                fixedwarnings = None
+            else:
+                previd, prevrev = prev
+
+                cur.execute('''SELECT warnings.signature, file, lineno, msg
+                               FROM warnings, wlines
+                               WHERE
+                                 warnings.buildnumber = wlines.buildnumber AND
+                                 warnings.signature = wlines.signature AND
+                                 wlines.wline = 0 AND
+                                 warnings.buildnumber = ? AND
+                                 NOT EXISTS (SELECT *
+                                             FROM warnings AS oldwarnings
+                                             WHERE oldwarnings.signature = warnings.signature AND
+                                             oldwarnings.buildnumber = ?)''', (id, previd))
+                newwarnings = cur.fetchall()
+
+                cur.execute('''SELECT warnings.signature, file, lineno, msg
+                               FROM warnings, wlines
+                               WHERE
+                                 warnings.buildnumber = wlines.buildnumber AND
+                                 warnings.signature = wlines.signature AND
+                                 wlines.wline = 0 AND
+                                 warnings.buildnumber = ? AND
+                                 NOT EXISTS (SELECT *
+                                             FROM warnings AS oldwarnings
+                                             WHERE oldwarnings.signature = warnings.signature AND
+                                             oldwarnings.buildnumber = ?)''', (previd, id))
+                fixedwarnings = cur.fetchall()
+                
+            return self.render('build.html',
+                               rev=rev,
+                               id=id,
+                               unique=unique,
+                               total=total,
+                               previd=previd,
+                               prevrev=prevrev,
+                               newwarnings=newwarnings,
+                               fixedwarnings=fixedwarnings)
+        finally:
+            cur.close()
+
+    @cherrypy.expose
+    def warning(self, signature):
+        try:
+            cur = self.dbcursor()
+            cur.execute('''SELECT buildnumber, rev
+                           FROM builds
+                           WHERE
+                             EXISTS (SELECT *
+                                     FROM warnings
+                                     WHERE
+                                       warnings.buildnumber = builds.buildnumber AND
+                                       warnings.signature = ?)
+                           ORDER BY buildnumber ASC LIMIT 1''', (signature,))
+
+            firstid, firstrev = cur.fetchone()
+            
+            cur.execute('''SELECT buildnumber, rev
+                           FROM builds
+                           WHERE
+                             NOT EXISTS (SELECT *
+                                         FROM warnings
+                                         WHERE
+                                           warnings.buildnumber = builds.buildnumber AND
+                                           warnings.signature = ?) AND
+                             buildnumber > ?
+                           ORDER BY buildnumber ASC LIMIT 1''', (signature, firstid))
+
+            lastid, lastrev = cur.fetchone() or (None, None)
+
+            cur.execute('''SELECT file, lineno, msg, blametype, blamefile, blamerev, blameline, blamewho
+                           FROM wlines
+                           WHERE
+                             buildnumber = ? AND
+                             signature = ?
+                           ORDER BY wline ASC''', (firstid, signature))
+
+            wlines = cur.fetchall()
+
+            return self.render('warning.html',
+                               firstid=firstid, firstrev=firstrev,
+                               lastid=lastid, lastrev=lastrev,
+                               wlines=wlines)
+        finally:
+            cur.close()
+
+def main(configfiles):
+    for cf in configfiles:
+        cherrypy.config.update(cf)
+    if 'tools.staticdir.root' not in cherrypy.config:
+        thisdir = os.path.abspath(os.path.dirname(__file__))
+        cherrypy.config.update({'tools.staticdir.root': thisdir})
+
+    app = cherrypy.tree.mount(Root(), '/', {
+        'global': {
+            'tools.encode.on': True,
+            'tools.encode.encoding': 'utf-8',
+        },
+        '/static': {
+            'tools.staticdir.on': True,
+            'tools.staticdir.dir': 'static',
+        },
+    })
+    for cf in configfiles:
+        app.merge(cf)
+
+    cherrypy.server.quickstart()
+    cherrypy.engine.start()
+
+if __name__ == '__main__':
+    main(sys.argv[1:])
new file mode 100644
--- /dev/null
+++ b/warning-ui/warning.html
@@ -0,0 +1,43 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:xi="http://www.w3.org/2001/XInclude"
+      xmlns:py="http://genshi.edgewall.org/">
+  <xi:include href="common.inc" />
+
+  <head>
+    <title>Warning: ${wlines[0][0]}:${wlines[0][1]}: ${wlines[0][2]}</title>
+    <link type="text/css" rel="stylesheet" href="/static/warnings.css" />
+  </head>
+  <body class="warning">
+    <h1>Warning: ${wlines[0][0]}:${wlines[0][1]}: ${wlines[0][2]}</h1>
+
+    <div class="h2block">
+      <h2>First appeared:</h2>
+      <a href="${genlink('build', id=firstid)}">${firstrev}</a>
+    </div>
+    <div class="h2block" py:if="lastid is not None">
+      <h2>Fixed in:</h2>
+      <a href="${genlink('build', id=lastid)}">${lastrev}</a>
+    </div>
+
+    <dl>
+      <py:for each="file, lineno, msg, blametype, blamefile, blamerev, blameline, blamewho in wlines">
+        <dt>
+          <a py:strip="blametype is None"
+             href="http://hg.mozilla.org/mozilla-central/file/${firstrev}/${file}#l${lineno}">
+            ${file}:${lineno}</a>:
+          ${msg}
+        </dt>
+        <dd py:if="blametype is not None">
+          <py:choose test="blametype">
+            <py:when test="'hg'">
+              Blamed on ${blamewho}: <a href="http://hg.mozilla.org/mozilla-central/annotate/${blamerev}/${blamefile}/#l${blameline}">${blamefile}:${blameline}</a>, revision <a href="http://hg.mozilla.org/mozilla-central/rev/${blamerev}">${blamerev}</a>
+            </py:when>
+            <py:when test="'cvs'">
+              Blamed in ${blamewho}: <a href="http://bonsai.mozilla.org/cvsblame.cgi?file=mozilla/${blamefile}&amp;rev=HG_REPO_INITIAL_IMPORT&amp;mark=${blameline}#${blameline}">${blamefile}:${blameline}, revision ${blamerev}</a>
+            </py:when>
+          </py:choose>
+        </dd>
+      </py:for>
+    </dl>
+  </body>
+</html>