# HG changeset patch # User cmlenz # Date 1123438363 0 # Node ID 86439c2aa6d6f42cf7efc947b10c9745257acbaf # Parent 16d69eb6e047b7fee72e4035b6e2f5cd525c85fa Store report data in BDB XML database. Closes #31. diff --git a/bitten/build/pythontools.py b/bitten/build/pythontools.py --- a/bitten/build/pythontools.py +++ b/bitten/build/pythontools.py @@ -56,7 +56,7 @@ r'(?P.*)$') msg_types = dict(W='warning', E='error', C='convention', R='refactor') - lint_elem = xmlio.Element('lint') + problems = xmlio.Element('problems') try: fd = open(ctxt.resolve(file), 'r') try: @@ -69,11 +69,11 @@ filename = filename[len(ctxt.basedir) + 1:] lineno = int(match.group('line')) tag = match.group('tag') - xmlio.SubElement(lint_elem, 'problem', type=type, tag=tag, + xmlio.SubElement(problems, 'problem', type=type, tag=tag, file=filename, line=lineno)[ match.group('msg') or '' ] - ctxt.report(lint_elem) + ctxt.report(problems) finally: fd.close() except IOError, e: diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -31,6 +31,7 @@ from trac.env import Environment from bitten.model import BuildConfig, TargetPlatform, Build, BuildStep, BuildLog +from bitten.store import ReportStore from bitten.util import archive, beep, xmlio log = logging.getLogger('bitten.master') @@ -354,6 +355,10 @@ message_elem.gettext())) build_log.insert(db=db) + store = ReportStore(self.env) + for report in elem.children('report'): + store.store_report(build, step, report) + def _build_completed(self, db, build, elem): log.info('Slave %s completed build %d ("%s" as of [%s])', self.name, build.id, build.config, build.rev) diff --git a/bitten/store.py b/bitten/store.py new file mode 100644 --- /dev/null +++ b/bitten/store.py @@ -0,0 +1,138 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# +# Bitten is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Trac is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# Author: Christopher Lenz + +import logging +import os + +from trac.core import * + +log = logging.getLogger('bitten.store') + + +class IReportStore(Interface): + + def store_report(build, step, xml): + """Store the given report.""" + + def retrieve_reports(build, step, type=None): + """Retrieve reports.""" + + +class ReportStore(Component): + + backends = ExtensionPoint(IReportStore) + + def _get_backend(self): + configured = self.config.get('bitten', 'report_store', 'BDBXMLStore') + for backend in self.backends: + if backend.__class__.__name__ == configured: + return backend + raise TracError, 'No report store backend available' + backend = property(fget=lambda self: self._get_backend()) + + def store_report(self, build, step, xml): + assert xml.name == 'report' and 'type' in xml.attr + self.backend.store_report(build, step, xml) + + def retrieve_reports(self, build, step, type=None): + return self.backend.retrieve_reports(build, step, type) + + +try: + import dbxml +except ImportError: + dbxml = None + + +class BDBXMLStore(Component): + implements(IReportStore) + + indexes = [ + ('build', 'node-metadata-equality-decimal'), + ('step', 'node-metadata-equality-string'), + ('type', 'node-attribute-equality-string'), + ('file', 'node-attribute-equality-string'), + ('line', 'node-attribute-equality-decimal') + ] + + + class XmlValueWrapper(object): + + _metadata = None + + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value.asString() + + def _get_metadata(self): + if self._metadata is None: + self._metadata = {} + for metadata in self.value.asDocument().getMetaDataIterator(): + if not metadata.get_uri(): + self._metadata[metadata.get_name()] = \ + metadata.get_value().asString() + return self._metadata + metadata = property(fget=lambda self: self._get_metadata()) + + + def __init__(self): + self.path = os.path.join(self.env.path, 'db', 'bitten.dbxml') + + def store_report(self, build, step, xml): + if dbxml is None: + log.warning('BDB XML not installed, cannot store report') + return + mgr = dbxml.XmlManager() + container = self._open_container(mgr, create=True) + ctxt = mgr.createUpdateContext() + doc = mgr.createDocument() + doc.setContent(str(xml)) + doc.setMetaData('', 'build', dbxml.XmlValue(build.id)) + doc.setMetaData('', 'step', dbxml.XmlValue(step.name)) + container.putDocument(doc, ctxt, dbxml.DBXML_GEN_NAME) + + def retrieve_reports(self, build, step, type=None): + if dbxml is None: + log.warning('BDB XML not installed, cannot retrieve reports') + return + path = os.path.join(self.env.path, 'db', 'bitten.dbxml') + mgr = dbxml.XmlManager() + container = self._open_container(mgr) + ctxt = mgr.createQueryContext() + query = "collection('%s')/report[dbxml:metadata('build')=%d and " \ + "dbxml:metadata('step')='%s'" \ + % (path, build.id, step.name) + if type is not None: + query += " and @type='%s'" % type + query += "]" + results = mgr.query(query, ctxt) + for value in results: + yield BDBXMLStore.XmlValueWrapper(value) + + def _open_container(self, mgr, create=False): + if create and not os.path.exists(self.path): + container = mgr.createContainer(self.path) + ctxt = mgr.createUpdateContext() + for name, index in self.indexes: + container.addIndex('', name, index, ctxt) + return container + return mgr.openContainer(self.path) diff --git a/bitten/tests/__init__.py b/bitten/tests/__init__.py --- a/bitten/tests/__init__.py +++ b/bitten/tests/__init__.py @@ -20,7 +20,7 @@ import unittest -from bitten.tests import model, recipe +from bitten.tests import model, recipe, store from bitten.util import tests as util from bitten.trac_ext import tests as trac_ext @@ -28,6 +28,7 @@ suite = unittest.TestSuite() suite.addTest(model.suite()) suite.addTest(recipe.suite()) + suite.addTest(store.suite()) suite.addTest(trac_ext.suite()) suite.addTest(util.suite()) return suite diff --git a/bitten/tests/store.py b/bitten/tests/store.py new file mode 100644 --- /dev/null +++ b/bitten/tests/store.py @@ -0,0 +1,66 @@ +# -*- coding: iso8859-1 -*- +# +# Copyright (C) 2005 Christopher Lenz +# +# Bitten is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# Trac is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. +# +# Author: Christopher Lenz + +import os +import shutil +import tempfile +import unittest + +from trac.test import EnvironmentStub, Mock +from bitten.store import BDBXMLStore +from bitten.util import xmlio + + +class BDBXMLStoreTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + self.env.path = tempfile.mkdtemp('bitten-test') + os.mkdir(os.path.join(self.env.path, 'db')) + + def tearDown(self): + shutil.rmtree(self.env.path) + + def test_store_report(self): + store = BDBXMLStore(self.env) + build = Mock(id=42) + step = Mock(name='foo') + xml = xmlio.Element('report', type='test')[xmlio.Element('dummy')] + store.store_report(build, step, xml) + + xml = xmlio.Element('report', type='lint')[xmlio.Element('dummy')] + store.store_report(build, step, xml) + + reports = list(store.retrieve_reports(build, step)) + self.assertEqual(2, len(reports)) + self.assertEqual('42', reports[0].metadata['build']) + self.assertEqual('foo', reports[0].metadata['step']) + self.assertEqual('42', reports[1].metadata['build']) + self.assertEqual('foo', reports[1].metadata['step']) + + reports = list(store.retrieve_reports(build, step, 'test')) + self.assertEqual(1, len(reports)) + + reports = list(store.retrieve_reports(build, step, 'lint')) + self.assertEqual(1, len(reports)) + + +def suite(): + return unittest.makeSuite(BDBXMLStoreTestCase, 'test') diff --git a/bitten/util/xmlio.py b/bitten/util/xmlio.py --- a/bitten/util/xmlio.py +++ b/bitten/util/xmlio.py @@ -207,6 +207,18 @@ def gettext(self): return ''.join([c.nodeValue for c in self._node.childNodes]) + def write(self, out, newlines=False): + """Serializes the element and writes the XML to the given output + stream. + """ + self._node.writexml(out, newl=newlines and '\n' or '') + + def __str__(self): + """Return a string representation of the XML element.""" + buf = StringIO() + self.write(buf) + return buf.getvalue() + if __name__ == '__main__': import doctest