# HG changeset patch # User osimons # Date 1250540994 0 # Node ID 21902b481ee7a15818d1474deee263dde9a75137 # Parent 5f09d36c24be77faf26016962d73a10edba593c0 0.6dev: Inline display of error and failure details in 'Test Results' summary table. Thanks to Mat Booth for patch! Closes #205. diff --git a/bitten/build/javatools.py b/bitten/build/javatools.py --- a/bitten/build/javatools.py +++ b/bitten/build/javatools.py @@ -116,6 +116,7 @@ for testcase in xmlio.parse(fileobj).children('testcase'): test = xmlio.Element('test') test.attr['fixture'] = testcase.attr['classname'] + test.attr['name'] = testcase.attr['name'] if 'time' in testcase.attr: test.attr['duration'] = testcase.attr['time'] if srcdir is not None: @@ -126,9 +127,16 @@ result = list(testcase.children()) if result: test.attr['status'] = result[0].name - test.append(xmlio.Element('traceback')[ - result[0].gettext() - ]) + # Sometimes the traceback isn't prefixed with the + # exception type and message, so add it in if needed + tracebackprefix = "%s: %s" % (result[0].attr['type'], + result[0].attr['message']) + if result[0].gettext().startswith(tracebackprefix): + test.append(xmlio.Element('traceback')[ + result[0].gettext()]) + else: + test.append(xmlio.Element('traceback')[ + "\n".join((tracebackprefix, result[0].gettext()))]) failed += 1 else: test.attr['status'] = 'success' diff --git a/bitten/htdocs/bitten.css b/bitten/htdocs/bitten.css --- a/bitten/htdocs/bitten.css +++ b/bitten/htdocs/bitten.css @@ -156,14 +156,27 @@ font-weight: bold; } #content.build table.tests tr.failed { background: #d99; } +#content.build table.tests tr.failed th, #content.build table.tests tr.failed td { font-weight: bold; } -#content.build table.tests tr.failed:hover th, -#content.build table.tests tr.failed:hover td, -#content.build table.tests tr.failed:hover tr { background: #966; } #content.build table.tests tr.failed :link, -#content.build table.tests tr.failed :visited { color: #933 } -#content.build table.tests tr.failed :link:hover, -#content.build table.tests tr.failed :visited:hover { color: #fff; } +#content.build table.tests tr.failed :visited { color: #b00 } + +/* collapsible failure details */ +#content.build table.tests tr th p { margin: 0; padding: 0; text-align: left; } +#content.build table.tests tr th p.details { + margin: 0; padding-left: 32px; padding-top: 5px; text-align: left; font-weight: normal; +} +#content.build table.tests tr th p.details span { + white-space: pre; font-family: monospace; font-weight: normal; color: #666; +} +#content.build table.tests .fixture { display: inline; } +#content.build table.tests tr.failed th .fixture a { + background: url(../common/expanded.png) 0 50% no-repeat; padding-left: 16px; +} +#content.build table.tests tr.failed th.collapsed .fixture a { + background-image: url(../common/collapsed.png); +} +#content.build table.tests tr.failed th.collapsed p.details { display: none; } #content.build .log { background: #fff; border: 1px inset; font-size: 90%; overflow: auto; max-height: 20em; width: 100%; white-space: pre; diff --git a/bitten/report/testing.py b/bitten/report/testing.py --- a/bitten/report/testing.py +++ b/bitten/report/testing.py @@ -9,6 +9,7 @@ # are also available at http://bitten.edgewall.org/wiki/License. from trac.core import * +from trac.web.chrome import add_script from bitten.api import IReportChartGenerator, IReportSummarizer __docformat__ = 'restructuredtext en' @@ -125,6 +126,42 @@ if file: fixtures[-1]['href'] = req.href.browser(config.path, file) + # For each fixture, get a list of tests that don't succeed + for fixture in fixtures: + cursor.execute(""" +SELECT item_status.value AS status, item_name.value AS name, + item_traceback.value AS traceback +FROM bitten_report + LEFT OUTER JOIN bitten_report_item AS item_fixture + ON (item_fixture.report=bitten_report.id AND + item_fixture.name='fixture') + LEFT OUTER JOIN bitten_report_item AS item_status + ON (item_status.report=bitten_report.id AND + item_status.item=item_fixture.item AND + item_status.name='status') + LEFT OUTER JOIN bitten_report_item AS item_name + ON (item_name.report=bitten_report.id AND + item_name.item=item_fixture.item AND + item_name.name='name') + LEFT OUTER JOIN bitten_report_item AS item_traceback + ON (item_traceback.report=bitten_report.id AND + item_traceback.item=item_fixture.item AND + item_traceback.name='traceback') +WHERE category='test' AND build=%s AND step=%s AND item_status.value<>'success' AND + item_fixture.value=%s""", (build.id, step.name, fixture['name'])) + + failures = [] + for status, name, traceback in cursor: + # use the fixture name if a name isn't supplied for the + # individual test + if not name: + name = fixture['name'] + failures.append({'status': status, + 'name': name, + 'traceback': traceback}) + if failures: + fixture['failures'] = failures + data = {'fixtures': fixtures, 'totals': {'success': total_success, 'ignore': total_ignore, diff --git a/bitten/report/tests/testing.py b/bitten/report/tests/testing.py --- a/bitten/report/tests/testing.py +++ b/bitten/report/tests/testing.py @@ -12,8 +12,10 @@ from trac.db import DatabaseManager from trac.test import EnvironmentStub, Mock +from trac.web.href import Href from bitten.model import * -from bitten.report.testing import TestResultsChartGenerator +from bitten.report.testing import TestResultsChartGenerator, \ + TestResultsSummarizer class TestResultsChartGeneratorTestCase(unittest.TestCase): @@ -98,9 +100,64 @@ self.assertEqual(2, data['data'][2][1]) +class TestResultsSummarizerTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = '' + + db = self.env.get_db_cnx() + cursor = db.cursor() + connector, _ = DatabaseManager(self.env)._get_connector() + for table in schema: + for stmt in connector.to_sql(table): + cursor.execute(stmt) + + def test_testcase_errors_and_failures(self): + config = Mock(name='trunk', path='/somewhere') + step = Mock(name='foo') + + build = Build(self.env, config=config.name, platform=1, rev=123, + rev_time=42) + build.insert() + report = Report(self.env, build=build.id, step=step.name, + category='test') + report.items += [{'fixture': 'test_foo', + 'name': 'foo', 'file': 'foo.c', + 'type': 'test', 'status': 'success'}, + {'fixture': 'test_bar', + 'name': 'bar', 'file': 'bar.c', + 'type': 'test', 'status': 'error', + 'traceback': 'Error traceback'}, + {'fixture': 'test_baz', + 'name': 'baz', 'file': 'baz.c', + 'type': 'test', 'status': 'failure', + 'traceback': 'Failure reason'}] + report.insert() + + req = Mock(href=Href('trac')) + generator = TestResultsSummarizer(self.env) + template, data = generator.render_summary(req, + config, build, step, 'test') + self.assertEquals('bitten_summary_tests.html', template) + self.assertEquals(data['totals'], + {'ignore': 0, 'failure': 1, 'success': 1, 'error': 1}) + for fixture in data['fixtures']: + if fixture.has_key('failures'): + if fixture['failures'][0]['status'] == 'error': + self.assertEquals('test_bar', fixture['name']) + self.assertEquals('Error traceback', + fixture['failures'][0]['traceback']) + if fixture['failures'][0]['status'] == 'failure': + self.assertEquals('test_baz', fixture['name']) + self.assertEquals('Failure reason', + fixture['failures'][0]['traceback']) + + def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(TestResultsChartGeneratorTestCase)) + suite.addTest(unittest.makeSuite(TestResultsSummarizerTestCase)) return suite if __name__ == '__main__': diff --git a/bitten/templates/bitten_build.html b/bitten/templates/bitten_build.html --- a/bitten/templates/bitten_build.html +++ b/bitten/templates/bitten_build.html @@ -8,6 +8,11 @@ $title +
diff --git a/bitten/templates/bitten_summary_tests.html b/bitten/templates/bitten_summary_tests.html --- a/bitten/templates/bitten_summary_tests.html +++ b/bitten/templates/bitten_summary_tests.html @@ -11,10 +11,16 @@ FailuresIgnoresErrors - - $item.name - $item.name + class="${item.failures and 'failed' or 'success'}"> + + + + $item.name + $item.name + +

$failure.name ($failure.status):
+ $failure.traceback +

${item.num_success + item.num_failure + item.num_error + item.num_ignore} $item.num_failure diff --git a/bitten/web_ui.py b/bitten/web_ui.py --- a/bitten/web_ui.py +++ b/bitten/web_ui.py @@ -545,6 +545,7 @@ chgset = repos.get_changeset(build.rev) data['build']['chgset_author'] = chgset.author + add_script(req, 'common/js/folding.js') add_script(req, 'bitten/tabset.js') add_stylesheet(req, 'bitten/bitten.css') return 'bitten_build.html', data, None