changeset 161:4677161d2ae9

Reports can now be "summarized" on the build results page, with special components rendering summary HTML fragments for specific report types. The summaries are displayed as tabs next to the log of the build step. Currently summarizers for test results and code coverage exist.
author cmlenz
date Sat, 27 Aug 2005 07:28:30 +0000
parents dd745d6b8c83
children 8d071396dc1f
files bitten/trac_ext/api.py bitten/trac_ext/htdocs/bitten.css bitten/trac_ext/htdocs/tabset.js bitten/trac_ext/templates/bitten_build.cs bitten/trac_ext/web_ui.py
diffstat 5 files changed, 146 insertions(+), 47 deletions(-) [+]
line wrap: on
line diff
--- a/bitten/trac_ext/api.py
+++ b/bitten/trac_ext/api.py
@@ -31,3 +31,16 @@
         The function must take two positional arguments, `level` and `message`,
         and return the formatted message.
         """
+
+
+class IReportSummarizer(Interface):
+    """Extension point interface for components that render a summary of reports
+    of some kind."""
+
+    def get_supported_report_types():
+        """Return a list of strings identifying the types of reports this 
+        component supports."""
+
+    def render_report_summary(req, build, step, report):
+        """Render a summary for the given report and return the results HTML as
+        a string."""
--- a/bitten/trac_ext/htdocs/bitten.css
+++ b/bitten/trac_ext/htdocs/bitten.css
@@ -19,16 +19,21 @@
 #content.build #builds td.failed { background: #d99; }
 #content.build #builds td.in-progress { background: #ff9; }
 
-#content.build .reports {
- background: #d7d7d7;
- float: right;
- font-size: 90%;
- margin: .5em 0 .5em 1em;
- padding: .5em;
-}
-#content.build .reports h3 { margin: 0; }
-#content.build .reports ul { margin: 0; padding: 0; list-style: none; }
+#content.build .tabs { list-style: none; float: left; width: 100%; margin: 0;
+ padding: 0; }
+#content.build .tabs li { cursor: pointer; float: left; }
+#content.build .tabs li a { background: #b9b9b9; color: #666; display: block;
+ margin: 2px 2px 0; padding: 3px 2em 0; }
+#content.build .tabs li a:hover { color: #333; text-decoration: none; }
+#content.build .tabs li.active a { background: #d7d7d7; border: 1px outset;
+ border-bottom: none; color: #333; font-weight: bold; margin-top: 0;
+ padding-bottom: 1px; }
+#content.build .tab-content { background: #fff; border: 1px outset;
+ clear: both; padding: 5px; }
+#content.build .tab-content table { margin: 0; }
 
-#content.build .log { clear: right; overflow: auto; white-space: pre; }
+#content.build .log { border: 1px inset; overflow: auto; max-height: 30em;
+ width: 100%; white-space: pre; }
+#content.build .log code { padding: 0 5px; }
 #content.build .log .warning { color: #660; font-weight: bold; }
 #content.build .log .error { color: #900; font-weight: bold; }
new file mode 100644
--- /dev/null
+++ b/bitten/trac_ext/htdocs/tabset.js
@@ -0,0 +1,54 @@
+function makeTabSet(parentElement) {
+  var tabList = document.createElement("ul");
+  tabList.className = "tabs";
+  var contentDivs = document.createElement("div");
+
+  function makeTab(div) {
+    var title = div.firstChild;
+    while (title.nodeType != 1) title = title.nextSibling;
+    var tabItem = document.createElement("li");
+    if (!tabList.childNodes.length) tabItem.className = "active";
+    var link = document.createElement("a");
+    link.href = "#";
+    link.appendChild(title.firstChild);
+    tabItem.appendChild(link);
+
+    var contentDiv = document.createElement("div");
+    contentDiv.className = "tab-content";
+    while (div.childNodes.length) contentDiv.appendChild(div.firstChild);
+    if (tabList.childNodes.length) contentDiv.style.display = "none";
+
+    link.onclick = function() {
+      var child = contentDivs.firstChild;
+      while (child) {
+        if (child != contentDiv && child.nodeType == 1) {
+          child.style.display = "none";
+        }
+        child = child.nextSibling;
+      }
+      var item = tabList.firstChild;
+      while (item) {
+        if (item.nodeType == 1) {
+          item.className = item != tabItem ? "" : "active";
+        }
+        item = item.nextSibling;
+      }
+      contentDiv.style.display = "block";
+      return false;
+    }
+    contentDivs.appendChild(contentDiv);
+    tabList.appendChild(tabItem);
+  }
+
+  var divs = parentElement.getElementsByTagName("div");
+  for (var i = 0; i < divs.length; i++) {
+    var div = divs[i];
+    if (!/\btab\b/.test(div.className)) {
+      continue;
+    }
+    makeTab(div);
+  }
+
+  parentElement.appendChild(tabList);
+  parentElement.appendChild(contentDivs);
+}
--- a/bitten/trac_ext/templates/bitten_build.cs
+++ b/bitten/trac_ext/templates/bitten_build.cs
@@ -10,24 +10,31 @@
     var:build.slave.os ?> <?cs var:build.slave.os.version ?><?cs
     if:build.slave.machine ?> on <?cs var:build.slave.machine ?><?cs
     /if ?>)</p>
+  <script type="text/javascript" src="<?cs
+    var:htdocs_location ?>tabset.js"></script>
   <p class="time">Completed: <?cs var:build.started ?> (<?cs
     var:build.started_delta ?> ago)<br />Took: <?cs var:build.duration ?></p><?cs
   each:step = build.steps ?>
    <h2 id="<?cs var:step.name ?>"><?cs var:step.name ?> (<?cs
-     var:step.duration ?>)</h2><?cs
-   if:len(step.reports) ?>
-    <div class="reports"><h3>Generated Reports</h3><ul><?cs
-     each:report = step.reports ?><li class="<?cs
-       var:report.type ?>"><a href="<?cs var:report.href ?>"><?cs
-       var:report.type ?></a></li><?cs
-     /each ?>
-    </ul></div><?cs
-   /if ?>
+     var:step.duration ?>)</h2>
    <p><?cs var:step.description ?></p>
-   <div class="log"><?cs
-    each:item = step.log ?><code class="<?cs var:item.level ?>"><?cs
-     var:item.message ?></code><br /><?cs
-    /each ?></div><?cs
+   <div id="<?cs var:step.name ?>_tabs">
+    <div class="tab"><h3>Log</h3><div class="log"><?cs
+     each:item = step.log ?><code class="<?cs var:item.level ?>"><?cs
+      var:item.message ?></code><br /><?cs
+     /each ?></div>
+    </div><?cs
+    each:report = step.reports ?><?cs
+     if:report.summary ?>
+      <div class="tab report <?cs var:report.type ?>">
+       <?cs var:report.summary ?>
+      </div><?cs
+     /if ?><?cs
+    /each ?>
+   </div>
+   <script type="text/javascript">
+     makeTabSet(document.getElementById("<?cs var:step.name ?>_tabs"));
+   </script><?cs
   /each ?>
  </div>
 <?cs include:"footer.cs" ?>
--- a/bitten/trac_ext/web_ui.py
+++ b/bitten/trac_ext/web_ui.py
@@ -31,7 +31,8 @@
 from trac.wiki import wiki_to_html
 from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, BuildLog
 from bitten.store import ReportStore
-from bitten.trac_ext.api import ILogFormatter
+from bitten.trac_ext.api import ILogFormatter, IReportSummarizer
+from bitten.trac_ext.summarizers import *
 
 _status_label = {Build.IN_PROGRESS: 'in progress',
                  Build.SUCCESS: 'completed',
@@ -390,6 +391,7 @@
     implements(INavigationContributor, IRequestHandler, ITimelineEventProvider)
 
     log_formatters = ExtensionPoint(ILogFormatter)
+    report_summarizers = ExtensionPoint(IReportSummarizer)
 
     # INavigationContributor methods
 
@@ -437,30 +439,10 @@
             steps.append({
                 'name': step.name, 'description': step.description,
                 'duration': pretty_timedelta(step.started, step.stopped),
-                'failed': step.status == BuildStep.FAILURE
+                'failed': step.status == BuildStep.FAILURE,
+                'log': self._render_log(req, build, step),
+                'reports': self._render_reports(req, build, step)
             })
-            for log in BuildLog.select(self.env, build=build.id,
-                                       step=step.name, db=db):
-                formatters = []
-                items = []
-                for formatter in self.log_formatters:
-                    formatters.append(formatter.get_formatter(req, build,
-                                                              step,
-                                                              log.type))
-                for level, message in log.messages:
-                    for format in formatters:
-                        message = format(level, message)
-                    items.append({'level': level, 'message': message})
-                steps[-1]['log'] = items
-
-            store = ReportStore(self.env)
-            reports = []
-            for report in store.retrieve_reports(build, step):
-                report_type = report.attr['type']
-                report_href = self.env.href.buildreport(build.id, step.name,
-                                                        report_type)
-                reports.append({'type': report_type, 'href': report_href})
-            steps[-1]['reports'] = reports
         req.hdf['build.steps'] = steps
 
         add_stylesheet(req, 'bitten.css')
@@ -496,6 +478,44 @@
                     href = self.env.href.build(config, id)
                 yield event_kinds[status], href, title, stopped, None, ''
 
+    # Internal methods
+
+    def _render_log(self, req, build, step):
+        items = []
+        for log in BuildLog.select(self.env, build=build.id, step=step.name):
+            formatters = []
+            for formatter in self.log_formatters:
+                formatters.append(formatter.get_formatter(req, build, step,
+                                                          log.type))
+            for level, message in log.messages:
+                for format in formatters:
+                    message = format(level, message)
+                items.append({'level': level, 'message': message})
+        return items
+
+    def _render_reports(self, req, build, step):
+        summarizers = {} # keyed by report type
+        for summarizer in self.report_summarizers:
+            types = summarizer.get_supported_report_types()
+            summarizers.update(dict([(type, summarizer) for type in types]))
+        self.log.debug("Report summarizers: %s", summarizers)
+
+        store = ReportStore(self.env)
+        reports = []
+        for report in store.retrieve_reports(build, step):
+            report_type = report.attr['type']
+            summarizer = summarizers.get(report_type)
+            if summarizer:
+                summary = summarizer.render_report_summary(req, build, step,
+                                                           report)
+            else:
+                summary = None
+            report_href = self.env.href.buildreport(build.id, step.name,
+                                                    report_type)
+            reports.append({'type': report_type, 'href': report_href,
+                            'summary': summary})
+        return reports
+
 
 class SourceFileLinkFormatter(Component):
     """Finds references to files and directories in the repository in the build
Copyright (C) 2012-2017 Edgewall Software