Mercurial > bitten > bitten-test
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