# HG changeset patch # User cmlenz # Date 1187694816 0 # Node ID 20ddfbb8e879505bce1cb77f19f72be6cba07944 # Parent 99d516f751697b8dc89d00471b1ee951cab3a5f3 Fix for #154, added unit tests. diff --git a/bitten/tests/admin.py b/bitten/tests/admin.py --- a/bitten/tests/admin.py +++ b/bitten/tests/admin.py @@ -17,6 +17,7 @@ from trac.test import EnvironmentStub, Mock from trac.web.href import Href from trac.web.main import RequestDone +from bitten.main import BuildSystem from bitten.model import BuildConfig, TargetPlatform, schema from bitten.admin import BuildMasterAdminPageProvider, \ BuildConfigurationsAdminPageProvider diff --git a/bitten/tests/web_ui.py b/bitten/tests/web_ui.py --- a/bitten/tests/web_ui.py +++ b/bitten/tests/web_ui.py @@ -12,13 +12,16 @@ import tempfile import unittest +from trac.core import TracError from trac.db import DatabaseManager from trac.perm import PermissionCache, PermissionSystem from trac.test import EnvironmentStub, Mock +from trac.util.html import Markup from trac.web.clearsilver import HDFWrapper from trac.web.href import Href -from bitten.model import BuildConfig, TargetPlatform, schema -from bitten.web_ui import BuildConfigController +from bitten.main import BuildSystem +from bitten.model import Build, BuildConfig, BuildStep, TargetPlatform, schema +from bitten.web_ui import BuildConfigController, SourceFileLinkFormatter class BuildConfigControllerTestCase(unittest.TestCase): @@ -121,8 +124,119 @@ req.hdf.get('chrome.links.next.0.href')) +class SourceFileLinkFormatterTestCase(unittest.TestCase): + + def setUp(self): + self.env = EnvironmentStub() + + # Create tables + 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) + + # Hook up a dummy repository + self.repos = Mock( + get_node=lambda path, rev=None: Mock(get_history=lambda: [], + isdir=True), + normalize_path=lambda path: path, + sync=lambda: None + ) + self.env.get_repository = lambda authname=None: self.repos + + def tearDown(self): + pass + + def test_format_simple_link_in_repos(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + self.repos.get_node = lambda path, rev: (path, rev) + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in ' + 'foo/bar.c', output) + + def test_format_simple_link_not_in_repos(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + def _raise(): + raise TracError('No such node') + self.repos.get_node = lambda path, rev: _raise() + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in foo/bar.c', output) + + def test_format_link_in_repos_with_line(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + self.repos.get_node = lambda path, rev: (path, rev) + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c:123') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in ' + 'foo/bar.c:123', output) + + def test_format_link_not_in_repos_with_line(self): + BuildConfig(self.env, name='test', path='trunk').insert() + build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, + status=Build.SUCCESS, slave='hal') + build.insert() + step = BuildStep(self.env, build=build.id, name='foo', + status=BuildStep.SUCCESS) + step.insert() + + def _raise(): + raise TracError('No such node') + self.repos.get_node = lambda path, rev: _raise() + + req = Mock(method='GET', href=Href('/trac'), authname='hal') + comp = SourceFileLinkFormatter(self.env) + formatter = comp.get_formatter(req, build) + + output = formatter(step, None, None, u'error in foo/bar.c:123') + self.assertEqual(Markup, type(output)) + self.assertEqual('error in foo/bar.c:123', output) + + def suite(): - return unittest.makeSuite(BuildConfigControllerTestCase, 'test') + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(BuildConfigControllerTestCase, 'test')) + suite.addTest(unittest.makeSuite(SourceFileLinkFormatterTestCase, 'test')) + return suite if __name__ == '__main__': unittest.main(defaultTest='suite') diff --git a/bitten/web_ui.py b/bitten/web_ui.py --- a/bitten/web_ui.py +++ b/bitten/web_ui.py @@ -23,6 +23,7 @@ from trac.Timeline import ITimelineEventProvider from trac.util import escape, pretty_timedelta, format_datetime, shorten_line, \ Markup +from trac.util.html import html from trac.web import IRequestHandler from trac.web.chrome import INavigationContributor, ITemplateProvider, \ add_link, add_stylesheet @@ -559,11 +560,12 @@ class SourceFileLinkFormatter(Component): """Detects references to files in the build log and renders them as links - to the repository browser.""" + to the repository browser. + """ implements(ILogFormatter) - _fileref_re = re.compile('(?P[\w.-]+(?:/[\w.-]+)+)(?P(:\d+))') + _fileref_re = re.compile('(?P[\w.-]+(?:/[\w.-]+)+)(?P(:\d+))?') def get_formatter(self, req, build): """Return the log message formatter function.""" @@ -571,6 +573,7 @@ repos = self.env.get_repository(req.authname) href = req.href.browser cache = {} + def _replace(m): filepath = posixpath.normpath(m.group('path').replace('\\', '/')) if not cache.get(filepath) is True: @@ -578,7 +581,7 @@ path = '' for part in parts: path = posixpath.join(path, part) - if not path in cache: + if path not in cache: try: repos.get_node(posixpath.join(config.path, path), build.rev) @@ -587,9 +590,22 @@ cache[path] = False if cache[path] is False: return m.group(0) - return '%s' % ( - href(config.path, filepath) + '#L' + m.group('line')[1:], - m.group(0)) + link = href(config.path, filepath) + if m.group('line'): + link += '#L' + m.group('line')[1:] + return Markup(html.A(m.group(0), href=link)) + def _formatter(step, type, level, message): - return self._fileref_re.sub(_replace, message) + buf = [] + offset = 0 + for mo in self._fileref_re.finditer(message): + start, end = mo.span() + if start > offset: + buf.append(message[offset:start]) + buf.append(_replace(mo)) + offset = end + if offset < len(buf): + buf.append(message[offset:]) + return Markup("").join(buf) + return _formatter