# HG changeset patch # User osimons # Date 1251115243 0 # Node ID 8c824b14e1c54b82815c3d97b4b479a40c376d16 # Parent 9331ab08000f2adc9aef73653f6430f44f6b51d8 0.6dev: Switching `master.py` to use new `self._send_response()` and `self._send_error() methods. Simplifies code, but most importantly for errors it allows a consistent method for transmitting plain-text error messages to the slave (that the slave will now output as part of debug logging). Raising Trac HTTP* errors actually causes full rendering of an HTML error page as response, which is both inefficient as well as making it near-impossible to extract the 'hidden' message from the master. diff --git a/bitten/master.py b/bitten/master.py --- a/bitten/master.py +++ b/bitten/master.py @@ -19,9 +19,7 @@ from trac.config import BoolOption, IntOption, Option from trac.core import * from trac.resource import ResourceNotFound -from trac.web import IRequestHandler, HTTPBadRequest, HTTPConflict, \ - HTTPForbidden, HTTPMethodNotAllowed, HTTPNotFound, \ - RequestDone +from trac.web import IRequestHandler, RequestDone from bitten.model import BuildConfig, Build, BuildStep, BuildLog, Report, \ TargetPlatform @@ -35,6 +33,13 @@ __docformat__ = 'restructuredtext en' +HTTP_BAD_REQUEST = 400 +HTTP_FORBIDDEN = 403 +HTTP_NOT_FOUND = 404 +HTTP_METHOD_NOT_ALLOWED = 405 +HTTP_CONFLICT = 409 + + class BuildMaster(Component): """Trac request handler implementation for the build master.""" @@ -88,12 +93,14 @@ if 'id' not in req.args: if req.method != 'POST': - raise HTTPMethodNotAllowed('Method not allowed') + self._send_error(req, HTTP_METHOD_NOT_ALLOWED, + 'Only POST allowed for build creation') return self._process_build_creation(req) build = Build.fetch(self.env, req.args['id']) if not build: - raise HTTPNotFound('No such build') + self._send_error(req, HTTP_NOT_FOUND, + 'No such build (%s)' % req.args['id']) config = BuildConfig.fetch(self.env, build.config) if not req.args['collection']: @@ -103,12 +110,31 @@ return self._process_build_initiation(req, config, build) if req.method != 'POST': - raise HTTPMethodNotAllowed('Method not allowed') + self._send_error(req, HTTP_METHOD_NOT_ALLOWED, + 'Method %s not allowed' % req.method) if req.args['collection'] == 'steps': return self._process_build_step(req, config, build) else: - raise HTTPNotFound('No such collection') + self._send_error(req, HTTP_NOT_FOUND, + "No such collection '%s'" % req.args['collection']) + + # Internal methods + + def _send_response(self, req, code=200, body='', headers=None): + """ Formats and sends the response, raising ``RequestDone``. """ + req.send_response(code) + headers = headers or {} + for header in headers: + req.send_header(header, headers[header]) + req.write(body) + raise RequestDone + + def _send_error(self, req, code=500, message=''): + """ Formats and sends the error, raising ``RequestDone``. """ + headers = {'Content-Type': 'text/plain', + 'Content-Length': str(len(message))} + self._send_response(req, code, body=message, headers=headers) def _process_build_creation(self, req): queue = BuildQueue(self.env, build_all=self.build_all, @@ -121,7 +147,7 @@ except xmlio.ParseError, e: self.log.error('Error parsing build initialization request: %s', e, exc_info=True) - raise HTTPBadRequest('XML parser error') + self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error') slavename = elem.attr['name'] properties = {'name': slavename, Build.IP_ADDRESS: req.remote_addr} @@ -146,15 +172,11 @@ build = queue.get_build_for_slave(slavename, properties) if not build: - req.send_response(204) - req.write('') - raise RequestDone + self._send_response(req, 204, '', {}) - req.send_response(201) - req.send_header('Content-Type', 'text/plain') - req.send_header('Location', req.abs_href.builds(build.id)) - req.write('Build pending') - raise RequestDone + self._send_response(req, 201, 'Build pending', headers={ + 'Content-Type': 'text/plain', + 'Location': req.abs_href.builds(build.id)}) def _process_build_cancellation(self, req, config, build): self.log.info('Build slave %r cancelled build %d', build.slave, @@ -172,9 +194,7 @@ for listener in BuildSystem(self.env).listeners: listener.build_aborted(build) - req.send_response(204) - req.write('') - raise RequestDone + self._send_response(req, 204, '', {}) def _process_build_initiation(self, req, config, build): self.log.info('Build slave %r initiated build %d', build.slave, @@ -198,14 +218,12 @@ self.log.info('Build slave %r initiated build %d', build.slave, build.id) - req.send_response(200) - req.send_header('Content-Type', 'application/x-bitten+xml') - req.send_header('Content-Length', str(len(body))) - req.send_header('Content-Disposition', + self._send_response(req, 200, body, headers={ + 'Content-Type': 'application/x-bitten+xml', + 'Content-Length': str(len(body)), + 'Content-Disposition': 'attachment; filename=recipe_%s_r%s.xml' % - (config.name, build.rev)) - req.write(body) - raise RequestDone + (config.name, build.rev)}) def _process_build_step(self, req, config, build): try: @@ -213,18 +231,19 @@ except xmlio.ParseError, e: self.log.error('Error parsing build step result: %s', e, exc_info=True) - raise HTTPBadRequest('XML parser error') + self._send_error(req, HTTP_BAD_REQUEST, 'XML parser error') stepname = elem.attr['step'] # make sure it's the right slave. if build.status != Build.IN_PROGRESS or \ build.slave_info.get(Build.IP_ADDRESS) != req.remote_addr: - raise HTTPForbidden('Build %s has been invalidated for host %s.' - % (build.id, req.remote_addr)) + self._send_error(req, HTTP_FORBIDDEN, + 'Build %s has been invalidated for host %s.' + % (build.id, req.remote_addr)) step = BuildStep.fetch(self.env, build=build.id, name=stepname) if step: - raise HTTPConflict('Build step already exists') + self._send_error(req, HTTP_CONFLICT, 'Build step already exists') recipe = Recipe(xmlio.parse(config.recipe)) index = None @@ -234,7 +253,8 @@ index = num current_step = recipe_step if index is None: - raise HTTPForbidden('No such build step') + self._send_error(req, HTTP_FORBIDDEN, + 'No such build step' % stepname) last_step = index == num self.log.debug('Slave %s (build %d) completed step %d (%s) with ' @@ -250,7 +270,7 @@ except ValueError, e: self.log.error('Error parsing build step timestamp: %s', e, exc_info=True) - raise HTTPBadRequest(e.args[0]) + self._send_error(req, HTTP_BAD_REQUEST, e.args[0]) if elem.attr['status'] == 'failure': self.log.warning('Build %s step %s failed', build.id, stepname) step.status = BuildStep.FAILURE @@ -330,13 +350,11 @@ listener.build_completed(build) body = 'Build step processed' - req.send_response(201) - req.send_header('Content-Type', 'text/plain') - req.send_header('Content-Length', str(len(body))) - req.send_header('Location', req.abs_href.builds(build.id, 'steps', - stepname)) - req.write(body) - raise RequestDone + self._send_response(req, 201, body, { + 'Content-Type': 'text/plain', + 'Content-Length': str(len(body)), + 'Location': req.abs_href.builds( + build.id, 'steps', stepname)}) def _parse_iso_datetime(string): diff --git a/bitten/slave.py b/bitten/slave.py --- a/bitten/slave.py +++ b/bitten/slave.py @@ -159,7 +159,13 @@ return resp except urllib2.HTTPError, e: if e.code >= 300: - log.warning('Server returned error %d: %s', e.code, e.msg) + if e.headers.getheader('Content-Type', '' + ).startswith('text/plain'): + content = e.read() + else: + content = 'no message available' + log.debug('Server returned error %d: %s (%s)', + e.code, e.msg, content) raise return e diff --git a/bitten/tests/master.py b/bitten/tests/master.py --- a/bitten/tests/master.py +++ b/bitten/tests/master.py @@ -19,8 +19,7 @@ from trac.perm import PermissionCache, PermissionSystem from trac.test import EnvironmentStub, Mock from trac.util.datefmt import to_datetime, utc -from trac.web.api import HTTPBadRequest, HTTPMethodNotAllowed, HTTPNotFound, \ - HTTPForbidden, RequestDone +from trac.web.api import RequestDone from trac.web.href import Href from bitten.master import BuildMaster @@ -92,47 +91,58 @@ module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('text/plain', outheaders['Content-Type']) - location = outheaders['Location'] - mo = re.match('http://example.org/trac/builds/(\d+)', location) - assert mo, 'Location was %r' % location - self.assertEqual('Build pending', outbody.getvalue()) - build = Build.fetch(self.env, int(mo.group(1))) - self.assertEqual(Build.IN_PROGRESS, build.status) - self.assertEqual('hal', build.slave) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(201, outheaders['Status']) + self.assertEqual('text/plain', outheaders['Content-Type']) + location = outheaders['Location'] + mo = re.match('http://example.org/trac/builds/(\d+)', location) + assert mo, 'Location was %r' % location + self.assertEqual('Build pending', outbody.getvalue()) + build = Build.fetch(self.env, int(mo.group(1))) + self.assertEqual(Build.IN_PROGRESS, build.status) + self.assertEqual('hal', build.slave) def test_create_build_invalid_xml(self): inheaders = {'Content-Type': 'application/x-bitten+xml'} inbody = StringIO('') + outheaders = {} + outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds', href=Href('/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), - get_header=lambda x: inheaders.get(x), read=inbody.read) + get_header=lambda x: inheaders.get(x), read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPBadRequest') - except HTTPBadRequest, e: - self.assertEqual('XML parser error', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(400, outheaders['Status']) + self.assertEqual('XML parser error', outbody.getvalue()) def test_create_build_no_post(self): + outheaders = {} + outbody = StringIO() req = Mock(method='GET', base_path='', path_info='/builds', href=Href('/trac'), remote_addr='127.0.0.1', args={}, - perm=PermissionCache(self.env, 'hal')) + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) + module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPMethodNotAllowed') - except HTTPMethodNotAllowed, e: - self.assertEqual('Method not allowed', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(405, outheaders['Status']) + self.assertEquals('Only POST allowed for build creation', + outbody.getvalue()) def test_create_build_no_match(self): inheaders = {'Content-Type': 'application/x-bitten+xml'} @@ -152,12 +162,11 @@ module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(204, outheaders['Status']) - self.assertEqual('', outbody.getvalue()) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(204, outheaders['Status']) + self.assertEqual('', outbody.getvalue()) def test_cancel_build(self): config = BuildConfig(self.env, 'test', path='somepath', active=True, @@ -179,17 +188,16 @@ module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(204, outheaders['Status']) - self.assertEqual('', outbody.getvalue()) - # Make sure the started timestamp has been set - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.PENDING, build.status) - assert not build.started + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(204, outheaders['Status']) + self.assertEqual('', outbody.getvalue()) + + # Make sure the started timestamp has been set + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.PENDING, build.status) + assert not build.started def test_initiate_build(self): config = BuildConfig(self.env, 'test', path='somepath', active=True, @@ -215,38 +223,42 @@ module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(200, outheaders['Status']) - self.assertEqual('90', outheaders['Content-Length']) - self.assertEqual('application/x-bitten+xml', - outheaders['Content-Type']) - self.assertEqual('attachment; filename=recipe_test_r123.xml', - outheaders['Content-Disposition']) - self.assertEqual('', - outbody.getvalue()) - # Make sure the started timestamp has been set - build = Build.fetch(self.env, build.id) - assert build.started + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(200, outheaders['Status']) + self.assertEqual('90', outheaders['Content-Length']) + self.assertEqual('application/x-bitten+xml', + outheaders['Content-Type']) + self.assertEqual('attachment; filename=recipe_test_r123.xml', + outheaders['Content-Disposition']) + self.assertEqual('', + outbody.getvalue()) + + # Make sure the started timestamp has been set + build = Build.fetch(self.env, build.id) + assert build.started def test_initiate_build_no_such_build(self): + outheaders = {} + outbody = StringIO() req = Mock(method='GET', base_path='', path_info='/builds/123', href=Href('/trac'), remote_addr='127.0.0.1', args={}, - perm=PermissionCache(self.env, 'hal')) + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPNotFound') - except HTTPNotFound, e: - self.assertEqual('No such build', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(404, outheaders['Status']) + self.assertEquals('No such build (123)', outbody.getvalue()) def test_process_unknown_collection(self): BuildConfig(self.env, 'test', path='somepath', active=True, @@ -254,18 +266,23 @@ build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42) build.insert() + outheaders = {} + outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/files/' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, - perm=PermissionCache(self.env, 'hal')) + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPNotFound') - except HTTPNotFound, e: - self.assertEqual('No such collection', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(404, outheaders['Status']) + self.assertEqual("No such collection 'files'", outbody.getvalue()) def test_process_build_step_success(self): recipe = """ @@ -296,24 +313,23 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('20', outheaders['Content-Length']) - self.assertEqual('text/plain', outheaders['Content-Type']) - self.assertEqual('Build step processed', outbody.getvalue()) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.SUCCESS, build.status) - assert build.stopped - assert build.stopped > build.started + self.assertRaises(RequestDone, module.process_request, req) - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) - self.assertEqual('foo', steps[0].name) - self.assertEqual(BuildStep.SUCCESS, steps[0].status) + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) def test_process_build_step_success_with_log(self): recipe = """ @@ -348,32 +364,31 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('20', outheaders['Content-Length']) - self.assertEqual('text/plain', outheaders['Content-Type']) - self.assertEqual('Build step processed', outbody.getvalue()) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.SUCCESS, build.status) - assert build.stopped - assert build.stopped > build.started + self.assertRaises(RequestDone, module.process_request, req) - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) - self.assertEqual('foo', steps[0].name) - self.assertEqual(BuildStep.SUCCESS, steps[0].status) + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) - logs = list(BuildLog.select(self.env, build=build.id, step='foo')) - self.assertEqual(1, len(logs)) - self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', - logs[0].generator) - self.assertEqual(2, len(logs[0].messages)) - self.assertEqual((u'info', u'Doing stuff'), logs[0].messages[0]) - self.assertEqual((u'error', u'Ouch that hurt'), logs[0].messages[1]) + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + + logs = list(BuildLog.select(self.env, build=build.id, step='foo')) + self.assertEqual(1, len(logs)) + self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', + logs[0].generator) + self.assertEqual(2, len(logs[0].messages)) + self.assertEqual((u'info', u'Doing stuff'), logs[0].messages[0]) + self.assertEqual((u'error', u'Ouch that hurt'), logs[0].messages[1]) def test_process_build_step_success_with_report(self): recipe = """ @@ -410,37 +425,36 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('20', outheaders['Content-Length']) - self.assertEqual('text/plain', outheaders['Content-Type']) - self.assertEqual('Build step processed', outbody.getvalue()) - - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.SUCCESS, build.status) - assert build.stopped - assert build.stopped > build.started - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) - self.assertEqual('foo', steps[0].name) - self.assertEqual(BuildStep.SUCCESS, steps[0].status) + self.assertRaises(RequestDone, module.process_request, req) - reports = list(Report.select(self.env, build=build.id, step='foo')) - self.assertEqual(1, len(reports)) - self.assertEqual('test', reports[0].category) - self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', - reports[0].generator) - self.assertEqual(1, len(reports[0].items)) - self.assertEqual({ - 'fixture': 'my.Fixture', - 'file': 'my/test/file.py', - 'stdout': 'Doing my thing', - 'type': 'test', - }, reports[0].items[0]) + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.SUCCESS, steps[0].status) + + reports = list(Report.select(self.env, build=build.id, step='foo')) + self.assertEqual(1, len(reports)) + self.assertEqual('test', reports[0].category) + self.assertEqual('http://bitten.cmlenz.net/tools/python#unittest', + reports[0].generator) + self.assertEqual(1, len(reports[0].items)) + self.assertEqual({ + 'fixture': 'my.Fixture', + 'file': 'my/test/file.py', + 'stdout': 'Doing my thing', + 'type': 'test', + }, reports[0].items[0]) def test_process_build_step_success_with_attach(self): # Parse input and create attachments for config + build @@ -552,19 +566,19 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPForbidden') - except HTTPForbidden, e: - self.assertEqual('Build 1 has been invalidated for host 127.0.0.1.', e.detail) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.IN_PROGRESS, build.status) - assert not build.stopped + self.assertRaises(RequestDone, module.process_request, req) - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(0, len(steps)) + self.assertEqual(403, outheaders['Status']) + self.assertEqual('Build 1 has been invalidated for host 127.0.0.1.', + outbody.getvalue()) + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.IN_PROGRESS, build.status) + assert not build.stopped + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(0, len(steps)) def test_process_build_step_invalidated_build(self): recipe = """ @@ -601,16 +615,15 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.IN_PROGRESS, build.status) - assert not build.stopped - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) + self.assertRaises(RequestDone, module.process_request, req) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.IN_PROGRESS, build.status) + assert not build.stopped + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) # invalidate the build. @@ -640,14 +653,15 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Build was invalidated. Should fail.'); - except HTTPForbidden, e: - self.assertEqual('Build 1 has been invalidated for host 127.0.0.1.', e.detail) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.PENDING, build.status) + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(403, outheaders['Status']) + self.assertEquals('Build 1 has been invalidated for host 127.0.0.1.', + outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.PENDING, build.status) def test_process_build_step_failure(self): recipe = """ @@ -678,24 +692,23 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('20', outheaders['Content-Length']) - self.assertEqual('text/plain', outheaders['Content-Type']) - self.assertEqual('Build step processed', outbody.getvalue()) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.FAILURE, build.status) - assert build.stopped - assert build.stopped > build.started + self.assertRaises(RequestDone, module.process_request, req) - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) - self.assertEqual('foo', steps[0].name) - self.assertEqual(BuildStep.FAILURE, steps[0].status) + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.FAILURE, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.FAILURE, steps[0].status) def test_process_build_step_failure_ignored(self): recipe = """ @@ -727,24 +740,23 @@ write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected RequestDone') - except RequestDone: - self.assertEqual(201, outheaders['Status']) - self.assertEqual('20', outheaders['Content-Length']) - self.assertEqual('text/plain', outheaders['Content-Type']) - self.assertEqual('Build step processed', outbody.getvalue()) - build = Build.fetch(self.env, build.id) - self.assertEqual(Build.SUCCESS, build.status) - assert build.stopped - assert build.stopped > build.started + self.assertRaises(RequestDone, module.process_request, req) - steps = list(BuildStep.select(self.env, build.id)) - self.assertEqual(1, len(steps)) - self.assertEqual('foo', steps[0].name) - self.assertEqual(BuildStep.FAILURE, steps[0].status) + self.assertEqual(201, outheaders['Status']) + self.assertEqual('20', outheaders['Content-Length']) + self.assertEqual('text/plain', outheaders['Content-Type']) + self.assertEqual('Build step processed', outbody.getvalue()) + + build = Build.fetch(self.env, build.id) + self.assertEqual(Build.SUCCESS, build.status) + assert build.stopped + assert build.stopped > build.started + + steps = list(BuildStep.select(self.env, build.id)) + self.assertEqual(1, len(steps)) + self.assertEqual('foo', steps[0].name) + self.assertEqual(BuildStep.FAILURE, steps[0].status) def test_process_build_step_invalid_xml(self): recipe = """ @@ -758,19 +770,24 @@ build.insert() inbody = StringIO("""""") + outheaders = {} + outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), - read=inbody.read) + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPBadRequest') - except HTTPBadRequest, e: - self.assertEqual('XML parser error', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(400, outheaders['Status']) + self.assertEquals('XML parser error', outbody.getvalue()) def test_process_build_step_invalid_datetime(self): recipe = """ @@ -788,20 +805,25 @@ time="sometime tomorrow maybe" duration="3.45"> """) + outheaders = {} + outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), - read=inbody.read) + read=inbody.read, + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPBadRequest') - except HTTPBadRequest, e: - self.assertEqual("Invalid ISO date/time 'sometime tomorrow maybe'", - e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEquals(400, outheaders['Status']) + self.assertEquals("Invalid ISO date/time 'sometime tomorrow maybe'", + outbody.getvalue()) def test_process_build_step_no_post(self): BuildConfig(self.env, 'test', path='somepath', active=True, @@ -810,18 +832,23 @@ started=42) build.insert() + outheaders = {} + outbody = StringIO() req = Mock(method='GET', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, - perm=PermissionCache(self.env, 'hal')) + perm=PermissionCache(self.env, 'hal'), + send_response=lambda x: outheaders.setdefault('Status', x), + send_header=lambda x, y: outheaders.setdefault(x, y), + write=outbody.write) module = BuildMaster(self.env) assert module.match_request(req) - try: - module.process_request(req) - self.fail('Expected HTTPMethodNotAllowed') - except HTTPMethodNotAllowed, e: - self.assertEqual('Method not allowed', e.detail) + + self.assertRaises(RequestDone, module.process_request, req) + + self.assertEqual(405, outheaders['Status']) + self.assertEqual('Method GET not allowed', outbody.getvalue()) def suite():