def test_process_build_step_invalid_xml(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42) build.insert() inbody = StringIO("""<result></rsleut>""") 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) 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)
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) # posix output = formatter(step, None, None, u'error in foo/bar.c:123: bad') self.assertEqual(Markup, type(output)) self.assertEqual('error in <a href="/trac/browser/trunk/foo/bar.c#L123">' 'foo/bar.c:123</a>: bad', output) # windows output = formatter(step, None, None, u'error in foo\\win.c:123: bad') self.assertEqual(Markup, type(output)) self.assertEqual(r'error in <a href="/trac/browser/trunk/foo/win.c#L123">' 'foo\win.c:123</a>: bad', output)
def test_process_build_step_invalid_datetime(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; build.insert() inbody = StringIO("""<result step="foo" status="success" time="sometime tomorrow maybe" duration="3.45"> </result>""") 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) 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)
def test_process_build_step_no_post(self): BuildConfig(self.env, 'test', path='somepath', active=True, recipe='<build></build>').insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, 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'), send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) module._start_new_step(build, 'foo').insert() assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) self.assertEqual(405, outheaders['Status']) self.assertEqual('Method GET not allowed', outbody.getvalue())
def populate(self): """Add a build for the next change on each build configuration to the queue. The next change is the latest repository check-in for which there isn't a corresponding build on each target platform. Repeatedly calling this method will eventually result in the entire change history of the build configuration being in the build queue. """ repos = self.env.get_repository() assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' db = self.env.get_db_cnx() builds = [] for config in BuildConfig.select(self.env, db=db): platforms = [] for platform, rev, build in collect_changes(repos, config, db): if not self.build_all and platform.id in platforms: # We've seen this platform already, so these are older # builds that should only be built if built_all=True self.log.debug('Ignoring older revisions for configuration ' '%r on %r', config.name, platform.name) break platforms.append(platform.id) if build is None: self.log.info('Enqueuing build of configuration "%s" at ' 'revision [%s] on %s', config.name, rev, platform.name) rev_time = to_timestamp(repos.get_changeset(rev).date) age = int(time.time()) - rev_time if self.stabilize_wait and age < self.stabilize_wait: self.log.info('Delaying build of revision %s until %s ' 'seconds pass. Current age is: %s ' 'seconds' % (rev, self.stabilize_wait, age)) continue build = Build(self.env, config=config.name, platform=platform.id, rev=str(rev), rev_time=rev_time) builds.append(build) for build in builds: try: build.insert(db=db) db.commit() except Exception, e: # really only want to catch IntegrityErrors raised when # a second slave attempts to add builds with the same # (config, platform, rev) as an existing build. self.log.info('Failed to insert build of configuration "%s" ' 'at revision [%s] on platform [%s]: %s', build.config, build.rev, build.platform, e) db.rollback()
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) # posix output = formatter(step, None, None, u'error in foo/bar.c:123: bad') self.assertEqual(Markup, type(output)) self.assertEqual( 'error in <a href="/trac/browser/trunk/foo/bar.c#L123">' 'foo/bar.c:123</a>: bad', output) # windows output = formatter(step, None, None, u'error in foo\\win.c:123: bad') self.assertEqual(Markup, type(output)) self.assertEqual( r'error in <a href="/trac/browser/trunk/foo/win.c#L123">' 'foo\win.c:123</a>: bad', output)
def test_insert(self): build = Build(self.env, config='test', rev='42', rev_time=12039, platform=1) build.slave_info.update({ Build.IP_ADDRESS: '127.0.0.1', Build.MAINTAINER: '*****@*****.**' }) build.insert() db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute( "SELECT config,rev,platform,slave,started,stopped,status" " FROM bitten_build WHERE id=%s" % build.id) self.assertEqual(('test', '42', 1, '', 0, 0, 'P'), cursor.fetchone()) cursor.execute("SELECT propname,propvalue FROM bitten_slave") expected = { Build.IP_ADDRESS: '127.0.0.1', Build.MAINTAINER: '*****@*****.**' } for propname, propvalue in cursor: self.assertEqual(expected[propname], propvalue)
def test_reset_orphaned_builds(self): BuildConfig(self.env, 'test').insert() platform = TargetPlatform(self.env, config='test', name='Foo') platform.insert() build1 = Build(self.env, config='test', platform=platform.id, rev=123, rev_time=42, status=Build.IN_PROGRESS, slave='heinz', started=time.time() - 600) # Started ten minutes ago build1.insert() build2 = Build(self.env, config='test', platform=platform.id, rev=124, rev_time=42, status=Build.IN_PROGRESS, slave='heinz', started=time.time() - 60) # Started a minute ago build2.insert() queue = BuildQueue(self.env, timeout=300) # 5 minutes timeout build = queue.reset_orphaned_builds() self.assertEqual(Build.PENDING, Build.fetch(self.env, build1.id).status) self.assertEqual(Build.IN_PROGRESS, Build.fetch(self.env, build2.id).status)
def test_view_build(self): config = BuildConfig(self.env, name='test', path='trunk') config.insert() platform = TargetPlatform(self.env, config='test', name='any') platform.insert() build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, status=Build.SUCCESS, slave='hal') build.insert() PermissionSystem(self.env).grant_permission('joe', 'BUILD_VIEW') req = Mock(method='GET', base_path='', cgi_location='', path_info='/build/test/1', href=Href('/trac'), args={}, chrome={}, authname='joe', perm=PermissionCache(self.env, 'joe')) root = Mock(get_entries=lambda: ['foo'], get_history=lambda: [('trunk', rev, 'edit') for rev in range(123, 111, -1)]) self.repos.get_node=lambda path, rev=None: root self.repos.get_changeset=lambda rev: Mock(author='joe') module = BuildController(self.env) assert module.match_request(req) _, data, _ = module.process_request(req) self.assertEqual('view_build', data['page_mode']) self.assertEquals(Resource('build', 'test/1'), data['context'].resource) self.assertEquals([], data['build']['attachments']['attachments']) self.assertEquals('/trac/attachment/build/test/1/', data['build']['attachments']['attach_href'])
def test_cancel_build(self): config = BuildConfig(self.env, 'test', path='somepath', active=True, recipe='<build></build>') config.insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, status=Build.IN_PROGRESS, started=42) build.insert() outheaders = {} outbody = StringIO() req = Mock(method='DELETE', base_path='', path_info='/builds/%d' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, 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, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) assert module.match_request(req) 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_format_bad_links(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'Linking -I../.. with ../libtool') self.assertEqual(Markup, type(output)) self.assertEqual('Linking -I../.. with ../libtool', output)
def test_process_build_step_invalid_xml(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42) build.insert() inbody = StringIO("""<result></rsleut>""") 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, send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) module._start_new_step(build, 'foo').insert() assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) self.assertEquals(400, outheaders['Status']) self.assertEquals('XML parser error', outbody.getvalue())
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: bad') self.assertEqual(Markup, type(output)) self.assertEqual('error in foo/bar.c:123: bad', output)
def test_process_unknown_collection(self): BuildConfig(self.env, 'test', path='somepath', active=True, recipe='<build></build>').insert() 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'), send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) assert module.match_request(req) 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_with_log(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.slave_info[Build.IP_ADDRESS] = '127.0.0.1'; build.insert() inbody = StringIO("""<result step="foo" status="success" time="2007-04-01T15:30:00.0000" duration="3.45"> <log generator="http://bitten.cmlenz.net/tools/python#unittest"> <message level="info">Doing stuff</message> <message level="error">Ouch that hurt</message> </log> </result>""") outheaders = {} outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), abs_href=Href('http://example.org/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), 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 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) 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 populate(self): """Add a build for the next change on each build configuration to the queue. The next change is the latest repository check-in for which there isn't a corresponding build on each target platform. Repeatedly calling this method will eventually result in the entire change history of the build configuration being in the build queue. """ db = self.env.get_db_cnx() builds = [] for config in BuildConfig.select(self.env, db=db): platforms = [] for platform, rev, build in collect_changes(config, db=db): if not self.build_all and platform.id in platforms: # We've seen this platform already, so these are older # builds that should only be built if built_all=True self.log.debug('Ignoring older revisions for configuration ' '%r on %r', config.name, platform.name) break platforms.append(platform.id) if build is None: self.log.info('Enqueuing build of configuration "%s" at ' 'revision [%s] on %s', config.name, rev, platform.name) _repos_name, repos, _repos_path = get_repos( self.env, config.path, None) rev_time = to_timestamp(repos.get_changeset(rev).date) age = int(time.time()) - rev_time if self.stabilize_wait and age < self.stabilize_wait: self.log.info('Delaying build of revision %s until %s ' 'seconds pass. Current age is: %s ' 'seconds' % (rev, self.stabilize_wait, age)) continue build = Build(self.env, config=config.name, platform=platform.id, rev=str(rev), rev_time=rev_time) builds.append(build) for build in builds: try: build.insert(db=db) db.commit() except Exception, e: # really only want to catch IntegrityErrors raised when # a second slave attempts to add builds with the same # (config, platform, rev) as an existing build. self.log.info('Failed to insert build of configuration "%s" ' 'at revision [%s] on platform [%s]: %s', build.config, build.rev, build.platform, e) db.rollback()
def populate(self): """Add a build for the next change on each build configuration to the queue. The next change is the latest repository check-in for which there isn't a corresponding build on each target platform. Repeatedly calling this method will eventually result in the entire change history of the build configuration being in the build queue. """ repos = self.env.get_repository() if hasattr(repos, 'sync'): repos.sync() db = self.env.get_db_cnx() builds = [] for config in BuildConfig.select(self.env, db=db): platforms = [] for platform, rev, build in collect_changes(repos, config, db): if not self.build_all and platform.id in platforms: # We've seen this platform already, so these are older # builds that should only be built if built_all=True self.log.debug('Ignoring older revisions for configuration ' '%r on %r', config.name, platform.name) break platforms.append(platform.id) if build is None: self.log.info('Enqueuing build of configuration "%s" at ' 'revision [%s] on %s', config.name, rev, platform.name) rev_time = repos.get_changeset(rev).date if isinstance(rev_time, datetime): # Trac>=0.11 from trac.util.datefmt import to_timestamp rev_time = to_timestamp(rev_time) age = int(time.time()) - rev_time if self.stabilize_wait and age < self.stabilize_wait: self.log.info('Delaying build of revision %s until %s ' 'seconds pass. Current age is: %s ' 'seconds' % (rev, self.stabilize_wait, age)) continue build = Build(self.env, config=config.name, platform=platform.id, rev=str(rev), rev_time=rev_time) builds.append(build) for build in builds: build.insert(db=db) db.commit()
def test_initiate_build(self): config = BuildConfig(self.env, 'test', path='somepath', active=True, recipe='<build><step id="s1"></step></build>') config.insert() platform = TargetPlatform(self.env, config='test', name="Unix") platform.rules.append(('family', 'posix')) platform.insert() build = Build(self.env, 'test', '123', platform.id, slave='hal', rev_time=42) build.insert() outheaders = {} outbody = StringIO() req = Mock(method='GET', base_path='', path_info='/builds/%d' % build.id, href=Href('/trac'), remote_addr='127.0.0.1', args={}, authname='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, form_token="12345", incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) self.assertEqual(200, outheaders['Status']) self.assertEqual('163', 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( '<build build="1" config="test" form_token="12345" ' 'name="hal" path="somepath" platform="Unix" ' 'reponame="" repopath="somepath" ' 'revision="123"><step id="s1"/></build>', outbody.getvalue()) # Make sure the started timestamp has been set build = Build.fetch(self.env, build.id) assert build.started
def test_delete_platform_with_pending_builds(self): """Check that deleting a platform with pending builds removes those pending builds""" db = self.env.get_db_cnx() platform = TargetPlatform(self.env, config='test', name='Linux') platform.insert() build = Build(self.env, config='test', platform=platform.id, rev='42', rev_time=12039) build.insert() platform.delete() pending = list(build.select(self.env, config='test', status=Build.PENDING)) self.assertEqual(0, len(pending))
def test_should_delete_build_platform_dont_exist(self): messages = [] self.env.log = Mock(info=lambda msg, *args: messages.append(msg)) config = BuildConfig(self.env, 'test', active=True) config.insert() build = Build(self.env, config=config.name, rev=42, platform="no-stuff", rev_time=123456) build.insert() queue = BuildQueue(self.env, build_all=True) self.assertEqual(True, queue.should_delete_build(build, self.repos)) self.assert_("platform no longer exists" in messages[0])
def test_process_build_step_failure_continue(self): recipe = """<build> <step id="foo" onerror="continue"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.slave_info[Build.TOKEN] = '123'; build.insert() inbody = StringIO("""<result step="foo" status="failure" time="2007-04-01T15:30:00.0000" duration="3.45"> </result>""") outheaders = {} outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), abs_href=Href('http://example.org/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), read=inbody.read, send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write, incookie=Cookie('trac_auth=123')) module = BuildMaster(self.env) module._start_new_step(build, 'foo').insert() assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) 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_next_pending_build_inactive_config(self): """ Make sure that builds for a deactived build config are not scheduled. """ BuildConfig(self.env, 'test').insert() platform = TargetPlatform(self.env, config='test', name='Foo') platform.insert() build = Build(self.env, config='test', platform=platform.id, rev=123, rev_time=42, status=Build.PENDING) build.insert() queue = BuildQueue(self.env) build = queue.get_build_for_slave('foobar', {}) self.assertEqual(None, build)
def test_next_pending_build_no_matching_slave(self): """ Make sure that builds for which there is no slave matching the target platform are not scheduled. """ BuildConfig(self.env, 'test', active=True).insert() build = Build(self.env, config='test', platform=1, rev=123, rev_time=42, status=Build.PENDING) build.insert() build_id = build.id queue = BuildQueue(self.env) build = queue.get_build_for_slave('foobar', {}) self.assertEqual(None, build)
def test_should_delete_build_outside_revision_range(self): messages = [] self.env.log = Mock(info=lambda msg, *args: messages.append(msg)) self.repos.rev_older_than = lambda rev1, rev2: rev1 < rev2 config = BuildConfig(self.env, 'test', active=True, min_rev=120, max_rev=123) config.insert() platform = TargetPlatform(self.env, config='test', name='stuff') platform.insert() build1 = Build(self.env, config=config.name, rev=42, platform=platform.id, rev_time=123456) build1.insert() build2 = Build(self.env, config=config.name, rev=10042, platform=platform.id, rev_time=123456) build2.insert() queue = BuildQueue(self.env, build_all=True) self.assertEqual(True, queue.should_delete_build(build1, self.repos)) self.assertEqual(True, queue.should_delete_build(build2, self.repos)) self.assert_("outside of the revision range" in messages[0]) self.assert_("outside of the revision range" in messages[1])
def test_should_delete_build_config_deactivated(self): messages = [] self.env.log = Mock(info=lambda msg, *args: messages.append(msg)) config = BuildConfig(self.env, 'test', active=False) config.insert() platform = TargetPlatform(self.env, config='test', name='stuff') platform.insert() build = Build(self.env, config=config.name, rev=42, platform=platform.id, rev_time=123456) build.insert() queue = BuildQueue(self.env, build_all=True) self.assertEqual(True, queue.should_delete_build(build, self.repos)) self.assert_("configuration is deactivated" in messages[0])
def test_should_delete_build_config_none(self): out = [] self.env.log = Mock( info=lambda msg, *args: out.extend([msg] + list(args))) platform = TargetPlatform(self.env, config='test', name='stuff') platform.insert() build = Build(self.env, config='does_not_exist', rev=42, platform=platform.id, rev_time=123456) build.insert() queue = BuildQueue(self.env, build_all=True) self.assertEqual(True, queue.should_delete_build(build, self.repos)) self.assertTrue("configuration is deactivated" in out[0]) self.assertEquals('unknown config "does_not_exist"', out[1])
def test_process_build_step_wrong_slave(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.slave_info[Build.TOKEN] = '123'; build.insert() inbody = StringIO("""<result step="foo" status="success" time="2007-04-01T15:30:00.0000" duration="3.45"> <log generator="http://bitten.edgewall.org/tools/python#unittest"> <message level="info">Doing stuff</message> <message level="error">Ouch that hurt</message> </log> </result>""") outheaders = {} outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), abs_href=Href('http://example.org/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), read=inbody.read, send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write, incookie=Cookie('trac_auth=')) module = BuildMaster(self.env) module._start_new_step(build, 'foo').insert() assert module.match_request(req) self.assertRaises(RequestDone, module.process_request, req) self.assertEqual(409, outheaders['Status']) self.assertEqual('Token mismatch (wrong slave): slave=, build=123', 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(1, len(steps))
def test_process_attach_build(self): body, content_type = encode_multipart_formdata({ 'description': 'baz baz', 'file': ('baz.txt', 'hello baz'), '__FORM_TOKEN': '123456'}) args = {} for k, v in dict(cgi.FieldStorage(fp=StringIO(body), environ={ 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': content_type}) ).items(): if v.filename: args[k] = v else: args[k] = v.value args.update({'collection': 'attach', 'member': 'build'}) self.assertTrue('file' in args) outheaders = {} outbody = StringIO() req = Mock(args=args, form_token='123456', authname='hal', remote_addr='127.0.0.1', send_response=lambda x: outheaders.setdefault('Status', x), send_header=lambda x, y: outheaders.setdefault(x, y), write=outbody.write) config = BuildConfig(self.env, 'test', path='somepath', active=True, recipe='') config.insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.insert() module = BuildMaster(self.env) self.assertRaises(RequestDone, module._process_attachment, req, config, build) self.assertEqual(201, outheaders['Status']) self.assertEqual('18', outheaders['Content-Length']) self.assertEqual('text/plain', outheaders['Content-Type']) self.assertEqual('Attachment created', outbody.getvalue()) build_atts = list(Attachment.select(self.env, 'build', 'test/1')) self.assertEquals(1, len(build_atts)) self.assertEquals('hal', build_atts[0].author) self.assertEquals('baz baz', build_atts[0].description) self.assertEquals('baz.txt', build_atts[0].filename) self.assertEquals('hello baz', build_atts[0].open().read())
def test_insert_no_config_or_rev_or_rev_time_or_platform(self): build = Build(self.env) self.assertRaises(AssertionError, build.insert) build = Build(self.env, rev='42', rev_time=12039, platform=1) self.assertRaises(AssertionError, build.insert) # No config build = Build(self.env, config='test', rev_time=12039, platform=1) self.assertRaises(AssertionError, build.insert) # No rev build = Build(self.env, config='test', rev='42', platform=1) self.assertRaises(AssertionError, build.insert) # No rev time build = Build(self.env, config='test', rev='42', rev_time=12039) self.assertRaises(AssertionError, build.insert) # No platform
def test_populate_not_build_all(self): self.env.get_repository = lambda authname=None: Mock( get_changeset=lambda rev: Mock(date=to_datetime(rev * 1000, utc)), get_node=lambda path, rev=None: Mock( get_entries=lambda: [Mock(), Mock()], get_history=lambda: [('somepath', 123, 'edit'), ('somepath', 121, 'edit'), ('somepath', 120, 'edit')] ), normalize_path=lambda path: path, rev_older_than=lambda rev1, rev2: rev1 < rev2 ) BuildConfig(self.env, 'test', path='somepath', active=True).insert() platform1 = TargetPlatform(self.env, config='test', name='P1') platform1.insert() platform2 = TargetPlatform(self.env, config='test', name='P2') platform2.insert() queue = BuildQueue(self.env) queue.populate() queue.populate() queue.populate() builds = list(Build.select(self.env, config='test')) builds.sort(lambda a, b: cmp(a.platform, b.platform)) self.assertEqual(2, len(builds)) self.assertEqual(platform1.id, builds[0].platform) self.assertEqual('123', builds[0].rev) self.assertEqual(platform2.id, builds[1].platform) self.assertEqual('123', builds[1].rev)
def test_insert_invalid_status(self): build = Build(self.env, config='test', rev='42', rev_time=12039, status='DUNNO') self.assertRaises(AssertionError, build.insert)
def reset_orphaned_builds(self): """Reset all in-progress builds to ``PENDING`` state if they've been running so long that the configured timeout has been reached. This is used to cleanup after slaves that have unexpectedly cancelled a build without notifying the master, or are for some other reason not reporting back status updates. """ if not self.timeout: # If no timeout is set, none of the in-progress builds can be # considered orphaned return db = self.env.get_db_cnx() now = int(time.time()) for build in Build.select(self.env, status=Build.IN_PROGRESS, db=db): if now - build.started < self.timeout: # This build has not reached the timeout yet, assume it's still # being executed # FIXME: ideally, we'd base this check on the last activity on # the build, not the start time continue build.status = Build.PENDING build.slave = None build.slave_info = {} build.started = 0 for step in list(BuildStep.select(self.env, build=build.id, db=db)): step.delete(db=db) build.update(db=db) db.commit()
def should_delete_build(self, build, repos): config = BuildConfig.fetch(self.env, build.config) platform = TargetPlatform.fetch(self.env, build.platform) # Platform may or may not exist anymore - get safe name for logging platform_name = platform and platform.name \ or 'unknown platform "%s"' % build.platform # Drop build if platform no longer exists if not platform: self.log.info('Dropping build of configuration "%s" at ' 'revision [%s] on %s because the platform no longer ' 'exists', config.name, build.rev, platform_name) return True # Ignore pending builds for deactived build configs if not config.active: self.log.info('Dropping build of configuration "%s" at ' 'revision [%s] on %s because the configuration is ' 'deactivated', config.name, build.rev, platform_name) return True # If not 'build_all', drop if a more recent revision is available if not self.build_all and \ len(list(Build.select(self.env, config=build.config, min_rev_time=build.rev_time, platform=build.platform))) > 1: self.log.info('Dropping build of configuration "%s" at revision [%s] ' 'on "%s" because a more recent build exists', config.name, build.rev, platform_name) return True return False
def test_get_build_for_slave(self): """ Make sure that a pending build of an activated configuration is scheduled for a slave that matches the target platform. """ BuildConfig(self.env, 'test', active=True).insert() platform = TargetPlatform(self.env, config='test', name='Foo') platform.insert() build = Build(self.env, config='test', platform=platform.id, rev=123, rev_time=42, status=Build.PENDING) build.insert() build_id = build.id queue = BuildQueue(self.env) build = queue.get_build_for_slave('foobar', {}) self.assertEqual(build_id, build.id)
def process_request(self, req): req.perm.assert_permission('BUILD_EXEC') if 'id' not in req.args: if req.method != 'POST': raise HTTPMethodNotAllowed('Method not allowed') return self._process_build_creation(req) build = Build.fetch(self.env, req.args['id']) if not build: raise HTTPNotFound('No such build') config = BuildConfig.fetch(self.env, build.config) if not req.args['collection']: if req.method == 'DELETE': return self._process_build_cancellation(req, config, build) else: return self._process_build_initiation(req, config, build) if req.method != 'POST': raise HTTPMethodNotAllowed('Method not allowed') if req.args['collection'] == 'steps': return self._process_build_step(req, config, build) else: raise HTTPNotFound('No such collection')
def should_delete_build(self, build, repos): config = BuildConfig.fetch(self.env, build.config) platform = TargetPlatform.fetch(self.env, build.platform) # Platform may or may not exist anymore - get safe name for logging platform_name = platform and platform.name \ or 'unknown platform "%s"' % build.platform # Drop build if platform no longer exists if not platform: self.log.info( 'Dropping build of configuration "%s" at ' 'revision [%s] on %s because the platform no longer ' 'exists', config.name, build.rev, platform_name) return True # Ignore pending builds for deactived build configs if not config.active: self.log.info( 'Dropping build of configuration "%s" at ' 'revision [%s] on %s because the configuration is ' 'deactivated', config.name, build.rev, platform_name) return True # If not 'build_all', drop if a more recent revision is available if not self.build_all and \ len(list(Build.select(self.env, config=build.config, min_rev_time=build.rev_time, platform=build.platform))) > 1: self.log.info( 'Dropping build of configuration "%s" at revision [%s] ' 'on "%s" because a more recent build exists', config.name, build.rev, platform_name) return True return False
def get_annotation_data(self, context): add_stylesheet(context.req, 'bitten/bitten_coverage.css') resource = context.resource self.log.debug("Looking for coverage report for %s@%s..." % ( resource.id, str(resource.version))) builds = Build.select(self.env, rev=resource.version) reports = [] for build in builds: config = BuildConfig.fetch(self.env, build.config) if not resource.id.startswith(config.branch): continue reports = Report.select(self.env, build=build.id, category='coverage') branch_in_config = resource.id[len(config.branch):] for report in reports: for item in report.items: if item.get('file') == branch_in_config: coverage = item.get('line_hits', '').split() if coverage: # Return first result with line data self.log.debug( "Coverage annotate for %s@%s: %s" % \ (resource.id, resource.version, coverage)) return coverage return []
def getBuilds(self, req, config=None, rev=None, platform=None, slave=None, status=None): """Retrieve a list of builds that match the selected criteria, with all build properties """ builds = [] for build in Build.select(self.env, config=config or None, rev=rev or None, platform=platform or None, slave=slave or None, status=status or None): if not build: continue builds.append({ 'id': build.id, 'config': build.config, 'rev': build.rev, 'platform': build.platform, 'slave': build.slave, 'started': build.started, 'stopped': build.stopped, 'last_activity': build.last_activity, 'rev_time': build.rev_time, 'status': build.status, }) return builds
def get_annotation_data(self, context): """add annotation data for lint""" context.perm.require('BUILD_VIEW') add_stylesheet(context.req, 'bitten/bitten_coverage.css') add_stylesheet(context.req, 'bitten/bitten_lintannotator.css') resource = context.resource # attempt to use the version passed in with the request, # otherwise fall back to the latest version of this file. try: version = context.req.args['rev'] except (KeyError, TypeError): version = resource.version self.log.debug('no version passed to get_annotation_data') builds = Build.select(self.env, rev=version) self.log.debug("Looking for lint report for %s@%s [%s]..." % (resource.id, str(resource.version), version)) self.itemid = 0 data = {} reports = None for build in builds: config = BuildConfig.fetch(self.env, build.config) if not resource.id.lstrip('/').startswith(config.path.lstrip('/')): self.log.debug('Skip build %s' % build) continue path_in_config = resource.id[len(config.path) + 1:].lstrip('/') reports = Report.select(self.env, build=build.id, category='lint') for report in reports: for item in report.items: if item.get('file') == path_in_config: line = item.get('line') if line: problem = { 'category': item.get('category', ''), 'tag': item.get('tag', ''), 'bid': build.id, 'rbuild': report.build, 'rstep': report.step, 'rid': report.id } data.setdefault(int(line), []).append(problem) if data: self.log.debug("Lint annotate for %s@%s: %s results" % \ (resource.id, resource.version, len(data))) return data if not builds: self.log.debug("No builds found") elif not reports: self.log.debug("No reports found") else: self.log.debug("No item of any report matched (%s)" % reports) return None
def test_process_build_step_wrong_slave(self): recipe = """<build> <step id="foo"> </step> </build>""" BuildConfig(self.env, 'test', path='somepath', active=True, recipe=recipe).insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42, started=42, status=Build.IN_PROGRESS) build.slave_info[Build.IP_ADDRESS] = '192.168.1.1'; build.insert() inbody = StringIO("""<result step="foo" status="success" time="2007-04-01T15:30:00.0000" duration="3.45"> <log generator="http://bitten.cmlenz.net/tools/python#unittest"> <message level="info">Doing stuff</message> <message level="error">Ouch that hurt</message> </log> </result>""") outheaders = {} outbody = StringIO() req = Mock(method='POST', base_path='', path_info='/builds/%d/steps/' % build.id, href=Href('/trac'), abs_href=Href('http://example.org/trac'), remote_addr='127.0.0.1', args={}, perm=PermissionCache(self.env, 'hal'), 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 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 steps = list(BuildStep.select(self.env, build.id)) self.assertEqual(0, len(steps))
def test_insert(self): build = Build(self.env, config='test', rev='42', rev_time=12039, platform=1) build.slave_info.update({Build.IP_ADDRESS: '127.0.0.1', Build.MAINTAINER: '*****@*****.**'}) build.insert() db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT config,rev,platform,slave,started,stopped,status" " FROM bitten_build WHERE id=%s" % build.id) self.assertEqual(('test', '42', 1, '', 0, 0, 'P'), cursor.fetchone()) cursor.execute("SELECT propname,propvalue FROM bitten_slave") expected = {Build.IP_ADDRESS: '127.0.0.1', Build.MAINTAINER: '*****@*****.**'} for propname, propvalue in cursor: self.assertEqual(expected[propname], propvalue)
def test_process_unknown_collection(self): BuildConfig(self.env, 'test', path='somepath', active=True, recipe='<build></build>').insert() build = Build(self.env, 'test', '123', 1, slave='hal', rev_time=42) build.insert() 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')) 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)
def deleteBuild(self, req, id_): """Delete a build referenced from its unique ID """ try: build = Build.fetch(self.env, id_) build.delete() return True except Exception: return False
def resource_exists(self, resource): config_name, build_id = self._parse_resource(resource.id) if build_id: build = Build.fetch(self.env, build_id) return build and build.exists or False elif config_name: config = BuildConfig.fetch(self.env, config_name) return config and config.exists or False return False
def process_request(self, req): req.perm.require('BUILD_VIEW') db = self.env.get_db_cnx() build_id = int(req.args.get('id')) build = Build.fetch(self.env, build_id, db=db) assert build, 'Build %s does not exist' % build_id if req.method == 'POST': if req.args.get('action') == 'invalidate': self._do_invalidate(req, build, db) req.redirect(req.href.build(build.config, build.id)) add_link(req, 'up', req.href.build(build.config), 'Build Configuration') data = {'title': 'Build %s - %s' % (build_id, _status_title[build.status]), 'page_mode': 'view_build', 'build': {}} config = BuildConfig.fetch(self.env, build.config, db=db) data['build']['config'] = { 'name': config.label, 'href': req.href.build(config.name) } formatters = [] for formatter in self.log_formatters: formatters.append(formatter.get_formatter(req, build)) summarizers = {} # keyed by report type for summarizer in self.report_summarizers: categories = summarizer.get_supported_categories() summarizers.update(dict([(cat, summarizer) for cat in categories])) data['build'].update(_get_build_data(self.env, req, build)) steps = [] for step in BuildStep.select(self.env, build=build.id, db=db): steps.append({ 'name': step.name, 'description': step.description, 'duration': pretty_timedelta(step.started, step.stopped), 'failed': step.status == BuildStep.FAILURE, 'errors': step.errors, 'log': self._render_log(req, build, formatters, step), 'reports': self._render_reports(req, config, build, summarizers, step) }) data['build']['steps'] = steps data['build']['can_delete'] = ('BUILD_DELETE' in req.perm) repos = self.env.get_repository(req.authname) repos.authz.assert_permission(config.path) chgset = repos.get_changeset(build.rev) data['build']['chgset_author'] = chgset.author add_script(req, 'bitten/tabset.js') add_stylesheet(req, 'bitten/bitten.css') return 'bitten_build.html', data, None
def test_insert_no_slave(self): build = Build(self.env, config='test', rev='42', rev_time=12039, platform=1) build.status = Build.SUCCESS self.assertRaises(AssertionError, build.insert) build.status = Build.FAILURE self.assertRaises(AssertionError, build.insert) build.status = Build.IN_PROGRESS self.assertRaises(AssertionError, build.insert) build.status = Build.PENDING build.insert()