$failure.name ($failure.status):
+ $failure.traceback
+
# 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 @@
$failure.name ($failure.status):
+ $failure.traceback
+