def test_process_deactivate_config(self): BuildConfig(self.env, name='foo', path='branches/foo', active=True).insert() BuildConfig(self.env, name='bar', path='branches/bar', active=True).insert() redirected_to = [] def redirect(url): redirected_to.append(url) raise RequestDone req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), abs_href=Href('http://example.org/'), redirect=redirect, authname='joe', args={'apply': ''}) provider = BuildConfigurationsAdminPageProvider(self.env) try: provider.render_admin_panel(req, 'bitten', 'configs', '') self.fail('Expected RequestDone') except RequestDone: self.assertEqual('http://example.org/admin/bitten/configs', redirected_to[0]) config = BuildConfig.fetch(self.env, name='foo') self.assertEqual(False, config.active) config = BuildConfig.fetch(self.env, name='bar') self.assertEqual(False, config.active)
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 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_process_update_config(self): BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', active=True).insert() redirected_to = [] def redirect(url): redirected_to.append(url) raise RequestDone req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), abs_href=Href('http://example.org/'), redirect=redirect, authname='joe', chrome={'warnings': [], 'notices': []}, href=Href('/'), args={'save': '', 'name': 'foo', 'label': 'Foobar', 'description': 'Thanks for all the fish!'}) provider = BuildConfigurationsAdminPageProvider(self.env) try: provider.render_admin_panel(req, 'bitten', 'configs', 'foo') self.fail('Expected RequestDone') except RequestDone: self.assertEqual(['Configuration Saved.'], req.chrome['notices']) self.assertEqual('http://example.org/admin/bitten/configs/foo', redirected_to[0]) config = BuildConfig.fetch(self.env, name='foo') self.assertEqual('Foobar', config.label) self.assertEqual('Thanks for all the fish!', config.description)
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 should_delete_build(self, build, repos): # Ignore pending builds for deactived build configs config = BuildConfig.fetch(self.env, build.config) if not config.active: target_platform = TargetPlatform.fetch(self.env, build.platform) if target_platform: target_platform_name = '"%s"' % (target_platform.name,) else: target_platform_name = 'unknown platform "%s"' % (build.platform,) log.info('Dropping build of configuration "%s" at ' 'revision [%s] on %s because the configuration is ' 'deactivated', config.name, build.rev, target_platform_name) return True # Stay within the revision limits of the build config if (config.min_rev and repos.rev_older_than(build.rev, config.min_rev)) \ or (config.max_rev and repos.rev_older_than(config.max_rev, build.rev)): # This minimum and/or maximum revision has changed since # this build was enqueued, so drop it log.info('Dropping build of configuration "%s" at revision [%s] on ' '"%s" because it is outside of the revision range of the ' 'configuration', config.name, build.rev, TargetPlatform.fetch(self.env, build.platform).name) return True return False
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 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 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_update_no_name(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) config = BuildConfig.fetch(self.env, 'test') config.name = None self.assertRaises(AssertionError, config.update)
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 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 _format_link(formatter, ns, name, label): build = Build.fetch(self.env, int(name)) if build: config = BuildConfig.fetch(self.env, build.config) title = "Build %d ([%s] of %s) by %s" % (build.id, build.rev, config.label, build.slave) return '<a class="build" href="%s" title="%s">%s</a>' % ( formatter.href.build(build.config, build.id), title, label, ) return label
def test_fetch(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) config = BuildConfig.fetch(self.env, name='test') assert config.exists self.assertEqual('test', config.name) self.assertEqual('trunk', config.path) self.assertEqual('Test', config.label) self.assertEqual(False, config.active)
def test_delete(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) config = BuildConfig.fetch(self.env, 'test') config.delete() self.assertEqual(False, config.exists) cursor.execute("SELECT * FROM bitten_config WHERE name=%s", ('test',)) self.assertEqual(None, cursor.fetchone())
def process_request(self, req): category = req.args.get('category') config = BuildConfig.fetch(self.env, name=req.args.get('config')) for generator in self.generators: if category in generator.get_supported_categories(): tmpl, data = generator.generate_chart_data(req, config, category) break else: raise TracError('Unknown report category "%s"' % category) return tmpl, data, 'text/xml'
def process_request(self, req): category = req.args.get('category') config = BuildConfig.fetch(self.env, name=req.args.get('config')) for generator in self.generators: if category in generator.get_supported_categories(): tmpl, data = generator.generate_chart_data( req, config, category) break else: raise TracError('Unknown report category "%s"' % category) return tmpl, data, 'text/xml'
def _format_link(formatter, ns, name, label): try: name = int(name) except ValueError: return label build = Build.fetch(self.env, name) if build: config = BuildConfig.fetch(self.env, build.config) title = 'Build %d ([%s] of %s) by %s' % (build.id, build.rev, config.label, build.slave) return '<a class="build" href="%s" title="%s">%s</a>' \ % (formatter.href.build(build.config, build.id), title, label) return label
def _format_link(formatter, ns, name, label): try: name = int(name) except ValueError: return label build = Build.fetch(self.env, name) if build: config = BuildConfig.fetch(self.env, build.config) title = 'Build %d ([%s] of %s) by %s' % ( build.id, build.rev, config.label, build.slave) return '<a class="build" href="%s" title="%s">%s</a>' \ % (formatter.href.build(build.config, build.id), title, label) return label
def get_formatter(self, req, build): """Return the log message formatter function.""" config = BuildConfig.fetch(self.env, name=build.config) repos = self.env.get_repository(authname=req.authname) assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' href = req.href.browser cache = {} def _replace(m): filepath = posixpath.normpath(m.group('path').replace('\\', '/')) if not cache.get(filepath) is True: parts = filepath.split('/') path = '' for part in parts: path = posixpath.join(path, part) if path not in cache: try: full_path = posixpath.join(config.path, path) full_path = posixpath.normpath(full_path) if full_path.startswith(config.path + "/") \ or full_path == config.path: repos.get_node(full_path, build.rev) cache[path] = True else: cache[path] = False except TracError: cache[path] = False if cache[path] is False: return m.group(0) link = href(config.path, filepath) if m.group('line'): link += '#L' + m.group('line')[1:] return Markup(tag.a(m.group(0), href=link)) def _formatter(step, type, level, 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(message): buf.append(message[offset:]) return Markup("").join(buf) return _formatter
def get_build_for_slave(self, name, properties): """Check whether one of the pending builds can be built by the build slave. :param name: the name of the slave :type name: `basestring` :param properties: the slave configuration :type properties: `dict` :return: the allocated build, or `None` if no build was found :rtype: `Build` """ self.log.debug('Checking for pending builds...') db = self.env.get_db_cnx() self.reset_orphaned_builds() # Iterate through pending builds by descending revision timestamp, to # avoid the first configuration/platform getting all the builds platforms = [p.id for p in self.match_slave(name, properties)] builds_to_delete = [] build_found = False for build in Build.select(self.env, status=Build.PENDING, db=db): config_path = BuildConfig.fetch(self.env, name=build.config).path _name, repos, _path = get_repos(self.env, config_path, None) if self.should_delete_build(build, repos): self.log.info('Scheduling build %d for deletion', build.id) builds_to_delete.append(build) elif build.platform in platforms: build_found = True break if not build_found: self.log.debug('No pending builds.') build = None # delete any obsolete builds for build_to_delete in builds_to_delete: build_to_delete.delete(db=db) if build: build.slave = name build.slave_info.update(properties) build.status = Build.IN_PROGRESS build.update(db=db) if build or builds_to_delete: db.commit() return build
def get_formatter(self, req, build): """Return the log message formatter function.""" config = BuildConfig.fetch(self.env, name=build.config) repos = self.env.get_repository(req.authname) href = req.href.browser cache = {} def _replace(m): return m.group(0) # filepath = posixpath.normpath(m.group('path').replace('\\', '/')) # if not cache.get(filepath) is True: # parts = filepath.split('/') # path = '' # for part in parts: # path = posixpath.join(path, part) # if path not in cache: # try: # full_path = posixpath.join(config.path, path) # full_path = posixpath.normpath(full_path) # if full_path.startswith(config.path + "/") \ # or full_path == config.path: # repos.get_node(full_path, # build.rev) # cache[path] = True # else: # cache[path] = False # except TracError: # cache[path] = False # if cache[path] is False: # return m.group(0) # link = href(config.branch, filepath) # if m.group('line'): # link += '#L' + m.group('line')[1:] # return Markup(tag.a(m.group(0), href=link#)) def _formatter(step, type, level, 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(message): buf.append(message[offset:]) return Markup("").join(buf) return _formatter
def test_update_name_with_platform(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) cursor.execute("INSERT INTO bitten_platform (config,name) " "VALUES (%s,%s)", ('test', 'NetBSD')) config = BuildConfig.fetch(self.env, 'test') config.name = 'foobar' config.update() cursor.execute("SELECT config,name FROM bitten_platform") self.assertEqual(('foobar', 'NetBSD'), cursor.fetchone()) self.assertEqual(None, cursor.fetchone())
def _remove_configs(self, req): req.perm.assert_permission('BUILD_DELETE') sel = req.args.get('sel') if not sel: raise TracError('No configuration selected') sel = isinstance(sel, list) and sel or [sel] db = self.env.get_db_cnx() for name in sel: config = BuildConfig.fetch(self.env, name, db=db) if not config: raise TracError('Configuration %r not found' % name) config.delete(db=db) db.commit()
def should_delete_build(self, build, repos): config = BuildConfig.fetch(self.env, build.config) config_name = config and config.name \ or 'unknown config "%s"' % 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 and 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 # Stay within the revision limits of the build config if (config.min_rev and repos.rev_older_than(build.rev, config.min_rev)) \ or (config.max_rev and repos.rev_older_than(config.max_rev, build.rev)): self.log.info( 'Dropping build of configuration "%s" at revision [%s] on ' '"%s" because it is outside of the revision range of the ' 'configuration', 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_update_name(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) config = BuildConfig.fetch(self.env, 'test') config.name = 'foobar' config.update() db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT name,path,label,active,description " "FROM bitten_config") self.assertEqual(('foobar', 'trunk', 'Test', 0, ''), cursor.fetchone()) self.assertEqual(None, cursor.fetchone())
def process_request(self, req): req.perm.assert_permission('BUILD_EXEC') if 'trac_auth' in req.incookie: slave_token = req.incookie['trac_auth'].value else: slave_token = req.session.sid if 'id' not in req.args: if req.method != 'POST': self._send_response( req, body='Only POST allowed for build creation.') return self._process_build_creation(req, slave_token) build = Build.fetch(self.env, req.args['id']) if not build: self._send_error(req, HTTP_NOT_FOUND, 'No such build (%s)' % req.args['id']) build_token = build.slave_info.get('token', '') if build_token != slave_token: self._send_error(req, HTTP_CONFLICT, 'Token mismatch (wrong slave): slave=%s, build=%s' \ % (slave_token, build_token)) 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': 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) elif req.args['collection'] == 'attach': return self._process_attachment(req, config, build) elif req.args['collection'] == 'keepalive': return self._process_keepalive(req, config, build) else: self._send_error( req, HTTP_NOT_FOUND, "No such collection '%s'" % req.args['collection'])
def process_request(self, req): req.perm.assert_permission('BUILD_EXEC') if 'trac_auth' in req.incookie: slave_token = req.incookie['trac_auth'].value else: slave_token = req.session.sid if 'id' not in req.args: if req.method != 'POST': self._send_response(req, body='Only POST allowed for build creation.') return self._process_build_creation(req, slave_token) build = Build.fetch(self.env, req.args['id']) if not build: self._send_error(req, HTTP_NOT_FOUND, 'No such build (%s)' % req.args['id']) build_token = build.slave_info.get('token', '') if build_token != slave_token: self._send_error(req, HTTP_CONFLICT, 'Token mismatch (wrong slave): slave=%s, build=%s' \ % (slave_token, build_token)) 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': 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) elif req.args['collection'] == 'attach': return self._process_attachment(req, config, build) elif req.args['collection'] == 'keepalive': return self._process_keepalive(req, config, build) else: self._send_error(req, HTTP_NOT_FOUND, "No such collection '%s'" % req.args['collection'])
def should_delete_build(self, build, repos): config = BuildConfig.fetch(self.env, build.config) config_name = config and config.name \ or 'unknown config "%s"' % 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 and 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 # Stay within the revision limits of the build config if (config.min_rev and repos.rev_older_than(build.rev, config.min_rev)) \ or (config.max_rev and repos.rev_older_than(config.max_rev, build.rev)): self.log.info('Dropping build of configuration "%s" at revision [%s] on ' '"%s" because it is outside of the revision range of the ' 'configuration', 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_timeline_events(self, req, start, stop, filters): if 'build' not in filters: return # Attachments (will be rendered by attachment module) for event in AttachmentModule(self.env).get_timeline_events( req, Resource('build'), start, stop): yield event start = to_timestamp(start) stop = to_timestamp(stop) add_stylesheet(req, 'bitten/bitten.css') db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT b.id,b.config,c.label,c.path, b.rev,p.name," "b.stopped,b.status FROM bitten_build AS b" " INNER JOIN bitten_config AS c ON (c.name=b.config) " " INNER JOIN bitten_platform AS p ON (p.id=b.platform) " "WHERE b.stopped>=%s AND b.stopped<=%s " "AND b.status IN (%s, %s) ORDER BY b.stopped", (start, stop, Build.SUCCESS, Build.FAILURE)) event_kinds = {Build.SUCCESS: 'successbuild', Build.FAILURE: 'failedbuild'} for id_, config, label, path, rev, platform, stopped, status in cursor: config_object = BuildConfig.fetch(self.env, config, db=db) repos_name, repos, repos_path = get_repos(self.env, config_object.path, req.authname) if not _has_permission(req.perm, repos, repos_path, rev=rev): continue errors = [] if status == Build.FAILURE: for step in BuildStep.select(self.env, build=id_, status=BuildStep.FAILURE, db=db): errors += [(step.name, error) for error in step.errors] yield (event_kinds[status], to_datetime(stopped, utc), None, (id_, config, label, display_rev(repos, rev), platform, status, errors))
def get_annotation_data(self, context): add_stylesheet(context.req, 'bitten/bitten_coverage.css') resource = context.resource 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.path): continue reports = Report.select(self.env, build=build.id, category='coverage') path_in_config = resource.id[len(config.path):].lstrip('/') for report in reports: for item in report.items: if item.get('file') == path_in_config: # TODO should aggregate coverage across builds return item.get('line_hits', '').split() return []
def get_resource_description(self, resource, format=None, context=None, **kwargs): config_name, build_id = self._parse_resource(resource.id) config = BuildConfig.fetch(self.env, config_name) config_label = config.label or config_name if context: if build_id: return tag.a('Build %d ("%s")' % (build_id, config_label), href=href.build(config_name, build_id)) elif config_name: return tag.a('Build Configuration "%s"' % config_label, href=href.build(config_name, build_id)) else: if build_id: return 'Build %d ("%s")' % (build_id, config_label) elif config_name: return 'Build Configuration "%s"' % config_label self.log.error("Unknown build/config resource.id: %s" % resource.id) return 'Unknown Build or Config'
def test_process_update_config(self): BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', active=True).insert() redirected_to = [] def redirect(url): redirected_to.append(url) raise RequestDone req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), abs_href=Href('http://example.org/'), redirect=redirect, authname='joe', chrome={ 'warnings': [], 'notices': [] }, href=Href('/'), args={ 'save': '', 'name': 'foo', 'label': 'Foobar', 'description': 'Thanks for all the fish!' }) provider = BuildConfigurationsAdminPageProvider(self.env) try: provider.render_admin_panel(req, 'bitten', 'configs', 'foo') self.fail('Expected RequestDone') except RequestDone: self.assertEqual(['Configuration Saved.'], req.chrome['notices']) self.assertEqual('http://example.org/admin/bitten/configs/foo', redirected_to[0]) config = BuildConfig.fetch(self.env, name='foo') self.assertEqual('Foobar', config.label) self.assertEqual('Thanks for all the fish!', config.description)
def test_update(self): db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("INSERT INTO bitten_config (name,path,label,active) " "VALUES (%s,%s,%s,%s)", ('test', 'trunk', 'Test', 0)) config = BuildConfig.fetch(self.env, 'test') config.path = 'some_branch' config.label = 'Updated' config.active = True config.description = 'Bla bla bla' config.update() db = self.env.get_db_cnx() cursor = db.cursor() cursor.execute("SELECT name,path,label,active,description " "FROM bitten_config") self.assertEqual(('test', 'some_branch', 'Updated', 1, 'Bla bla bla'), cursor.fetchone()) self.assertEqual(None, cursor.fetchone())
def _format_link(formatter, ns, name, label): segments = name.split('#') name = segments[0] step = len(segments) == 2 and segments[1] or '' try: name = int(name) except ValueError: return label build = Build.fetch(self.env, name) if build: config = BuildConfig.fetch(self.env, build.config) title = 'Build %d ([%s] of %s) by %s' % (build.id, build.rev, config.label, build.slave) if step: if not step.startswith('step_'): step = 'step_' + step step = '#' + escape(step) return '<a class="build" href="%s" title="%s">%s</a>' \ % (formatter.href.build(build.config, build.id) + step, title, label) return label
def _format_link(formatter, ns, name, label): segments = name.split('#') name = segments[0] step = len(segments) == 2 and segments[1] or '' try: name = int(name) except ValueError: return label build = Build.fetch(self.env, name) if build: config = BuildConfig.fetch(self.env, build.config) title = 'Build %d ([%s] of %s) by %s' % ( build.id, build.rev, config.label, build.slave) if step: if not step.startswith('step_'): step = 'step_' + step step = '#' + escape(step) return '<a class="build" href="%s" title="%s">%s</a>' \ % (formatter.href.build(build.config, build.id) + step, title, label) return label
def get_resource_description(self, resource, format=None, context=None, **kwargs): config_name, build_id = self._parse_resource(resource.id) config = BuildConfig.fetch(self.env, config_name) config_label = config and config.label and config.label or config_name if context: if build_id: return tag.a('Build %d ("%s")' % (build_id, config_label), href=href.build(config_name, build_id)) elif config_name: return tag.a('Build Configuration "%s"' % config_label, href=href.build(config_name, build_id)) else: if build_id: return 'Build %d ("%s")' % (build_id, config_label) elif config_name: return 'Build Configuration "%s"' % config_label self.log.error("Unknown build/config resource.id: %s" % resource.id) return 'Unknown Build or Config'
def test_process_remove_config(self): BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', active=True).insert() BuildConfig(self.env, name='bar', label='Bar', path='branches/bar', min_rev='123', max_rev='456').insert() redirected_to = [] def redirect(url): redirected_to.append(url) raise RequestDone req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), abs_href=Href('http://example.org/'), redirect=redirect, args={'remove': '', 'sel': 'bar'}) provider = BuildConfigurationsAdminPageProvider(self.env) try: provider.render_admin_panel(req, 'bitten', 'configs', '') self.fail('Expected RequestDone') except RequestDone: self.assertEqual('http://example.org/admin/bitten/configs', redirected_to[0]) assert not BuildConfig.fetch(self.env, name='bar')
def test_process_remove_config(self): BuildConfig(self.env, name='foo', label='Foo', path='branches/foo', active=True).insert() BuildConfig(self.env, name='bar', label='Bar', path='branches/bar', min_rev='123', max_rev='456').insert() redirected_to = [] def redirect(url): redirected_to.append(url) raise RequestDone req = Mock(method='POST', perm=PermissionCache(self.env, 'joe'), abs_href=Href('http://example.org/'), redirect=redirect, args={ 'remove': '', 'sel': 'bar' }) provider = BuildConfigurationsAdminPageProvider(self.env) try: provider.render_admin_panel(req, 'bitten', 'configs', '') self.fail('Expected RequestDone') except RequestDone: self.assertEqual('http://example.org/admin/bitten/configs', redirected_to[0]) assert not BuildConfig.fetch(self.env, name='bar')
def get_change(self): config_path = BuildConfig.fetch(self.env, name=self.build.config).path reposname, repos, _path = get_repos(self.env, config_path, None) return reposname, repos, repos.get_changeset(self.build.rev)
def _render_config(self, req, config_name): db = self.env.get_db_cnx() config = BuildConfig.fetch(self.env, config_name, db=db) if not config: raise HTTPNotFound("Build configuration '%s' does not exist." \ % config_name) repos = self.env.get_repository(req.authname) repos.authz.assert_permission(config.branch) data = {'title': 'Build Configuration "%s"' \ % config.label or config.name, 'page_mode': 'view_config'} add_link(req, 'up', req.href.build(), 'Build Status') description = config.description if description: description = wiki_to_html(description, self.env, req) pending_builds = list( Build.select(self.env, config=config.name, status=Build.PENDING)) inprogress_builds = list( Build.select(self.env, config=config.name, status=Build.IN_PROGRESS)) rev = '' for b in repos.git.get_branches(): if b[0] == config.branch: rev = b[1] break data['config'] = { 'name': config.name, 'label': config.label, 'branch': config.branch, 'active': config.active, 'description': description, 'browser_href': req.href.browser(rev=rev), 'builds_pending': len(pending_builds), 'builds_inprogress': len(inprogress_builds) } context = Context.from_request(req, config.resource) data['context'] = context data['config']['attachments'] = AttachmentModule( self.env).attachment_data(context) platforms = list( TargetPlatform.select(self.env, config=config_name, db=db)) data['config']['platforms'] = [{ 'name': platform.name, 'id': platform.id, 'builds_pending': len( list( Build.select(self.env, config=config.name, status=Build.PENDING, platform=platform.id))), 'builds_inprogress': len( list( Build.select(self.env, config=config.name, status=Build.IN_PROGRESS, platform=platform.id))) } for platform in platforms] has_reports = False for report in Report.select(self.env, config=config.name, db=db): has_reports = True break if has_reports: chart_generators = [] for generator in ReportChartController(self.env).generators: for category in generator.get_supported_categories(): chart_generators.append({ 'href': req.href.build(config.name, 'chart/' + category) }) data['config']['charts'] = chart_generators charts_license = self.config.get('bitten', 'charts_license') if charts_license: data['config']['charts_license'] = charts_license page = max(1, int(req.args.get('page', 1))) more = False data['page_number'] = page repos = self.env.get_repository(req.authname) builds_per_page = 12 * len(platforms) idx = 0 builds = {} for platform, rev, build in collect_changes(repos, config): if idx >= page * builds_per_page: more = True break elif idx >= (page - 1) * builds_per_page: builds.setdefault(rev, {}) builds[rev].setdefault('href', req.href.changeset(rev)) if build and build.status != Build.PENDING: build_data = _get_build_data(self.env, req, build) build_data['steps'] = [] for step in BuildStep.select(self.env, build=build.id, db=db): build_data['steps'].append({ 'name': step.name, 'description': step.description, 'duration': to_datetime(step.stopped, utc) - \ to_datetime(step.started, utc), 'failed': not step.successful, 'errors': step.errors, 'href': build_data['href'] + '#step_' + step.name }) builds[rev][platform.id] = build_data idx += 1 data['config']['builds'] = builds if page > 1: if page == 2: prev_href = req.href.build(config.name) else: prev_href = req.href.build(config.name, page=page - 1) add_link(req, 'prev', prev_href, 'Previous Page') if more: next_href = req.href.build(config.name, page=page + 1) add_link(req, 'next', next_href, 'Next Page') prevnext_nav(req, 'Page') return data
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) if not build: raise HTTPNotFound("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 or config.name, 'href': req.href.build(config.name) } context = Context.from_request(req, build.resource) data['context'] = context data['build']['attachments'] = AttachmentModule( self.env).attachment_data(context) 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 \ and build.status != build.PENDING) repos = self.env.get_repository(req.authname) repos.authz.assert_permission(config.branch) chgset = repos.get_changeset(build.rev) data['build']['chgset_author'] = chgset.author add_script(req, 'common/js/folding.js') add_script(req, 'bitten/tabset.js') add_stylesheet(req, 'bitten/bitten.css') return 'bitten_build.html', data, None
def test_fetch_none(self): config = BuildConfig.fetch(self.env, name='test') self.assertEqual(None, config)
def _render_config(self, req, config_name): db = self.env.get_db_cnx() config = BuildConfig.fetch(self.env, config_name, db=db) if not config: raise HTTPNotFound("Build configuration '%s' does not exist." \ % config_name) repos = self.env.get_repository(authname=req.authname) assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' rev = config.max_rev or repos.youngest_rev try: _has_permission(req.perm, repos, config.path, rev=rev, raise_error=True) except NoSuchNode: raise TracError("Permission checking against repository path %s " "at revision %s failed." % (config.path, rev)) data = {'title': 'Build Configuration "%s"' \ % config.label or config.name, 'page_mode': 'view_config'} add_link(req, 'up', req.href.build(), 'Build Status') description = config.description if description: description = wiki_to_html(description, self.env, req) pending_builds = list( Build.select(self.env, config=config.name, status=Build.PENDING)) inprogress_builds = list( Build.select(self.env, config=config.name, status=Build.IN_PROGRESS)) data['config'] = { 'name': config.name, 'label': config.label, 'path': config.path, 'min_rev': config.min_rev, 'min_rev_href': req.href.changeset(config.min_rev), 'max_rev': config.max_rev, 'max_rev_href': req.href.changeset(config.max_rev), 'active': config.active, 'description': description, 'browser_href': req.href.browser(config.path), 'builds_pending': len(pending_builds), 'builds_inprogress': len(inprogress_builds) } context = Context.from_request(req, config.resource) data['context'] = context data['config']['attachments'] = AttachmentModule( self.env).attachment_data(context) platforms = list( TargetPlatform.select(self.env, config=config_name, db=db)) data['config']['platforms'] = [{ 'name': platform.name, 'id': platform.id, 'builds_pending': len( list( Build.select(self.env, config=config.name, status=Build.PENDING, platform=platform.id))), 'builds_inprogress': len( list( Build.select(self.env, config=config.name, status=Build.IN_PROGRESS, platform=platform.id))) } for platform in platforms] has_reports = False for report in Report.select(self.env, config=config.name, db=db): has_reports = True break if has_reports: chart_generators = [] report_categories = list( self._report_categories_for_config(config)) for generator in ReportChartController(self.env).generators: for category in generator.get_supported_categories(): if category in report_categories: chart_generators.append({ 'href': req.href.build(config.name, 'chart/' + category), 'category': category, 'style': self.config.get('bitten', 'chart_style'), }) data['config']['charts'] = chart_generators page = max(1, int(req.args.get('page', 1))) more = False data['page_number'] = page repos = self.env.get_repository(authname=req.authname) assert repos, 'No "(default)" Repository: Add a repository or alias ' \ 'named "(default)" to Trac.' builds_per_page = 12 * len(platforms) idx = 0 builds = {} revisions = [] for platform, rev, build in collect_changes(repos, config): if idx >= page * builds_per_page: more = True break elif idx >= (page - 1) * builds_per_page: if rev not in builds: revisions.append(rev) builds.setdefault(rev, {}) builds[rev].setdefault('href', req.href.changeset(rev)) builds[rev].setdefault('display_rev', repos.normalize_rev(rev)) if build and build.status != Build.PENDING: build_data = _get_build_data(self.env, req, build) build_data['steps'] = [] for step in BuildStep.select(self.env, build=build.id, db=db): build_data['steps'].append({ 'name': step.name, 'description': step.description, 'duration': to_datetime(step.stopped or int(time.time()), utc) - \ to_datetime(step.started, utc), 'status': _step_status_label[step.status], 'cls': _step_status_label[step.status].replace(' ', '-'), 'errors': step.errors, 'href': build_data['href'] + '#step_' + step.name }) builds[rev][platform.id] = build_data idx += 1 data['config']['builds'] = builds data['config']['revisions'] = revisions if page > 1: if page == 2: prev_href = req.href.build(config.name) else: prev_href = req.href.build(config.name, page=page - 1) add_link(req, 'prev', prev_href, 'Previous Page') if more: next_href = req.href.build(config.name, page=page + 1) add_link(req, 'next', next_href, 'Next Page') if arity(prevnext_nav) == 4: # Trac 0.12 compat, see #450 prevnext_nav(req, 'Previous Page', 'Next Page') else: prevnext_nav(req, 'Page') return data
def render_admin_panel(self, req, cat, page, path_info): data = {} # Analyze url try: config_name, platform_id = path_info.split('/', 1) except: config_name = path_info platform_id = None if config_name: # Existing build config warnings = [] if platform_id or ( # Editing or creating one of the config's target platforms req.method == 'POST' and 'new' in req.args): if platform_id: # Editing target platform platform_id = int(platform_id) platform = TargetPlatform.fetch(self.env, platform_id) if req.method == 'POST': if 'cancel' in req.args or \ self._update_platform(req, platform): req.redirect( req.abs_href.admin(cat, page, config_name)) else: # creating target platform platform = self._create_platform(req, config_name) req.redirect( req.abs_href.admin(cat, page, config_name, platform.id)) # Set up template variables data['platform'] = { 'id': platform.id, 'name': platform.name, 'exists': platform.exists, 'rules': [{ 'property': propname, 'pattern': pattern } for propname, pattern in platform.rules] or [('', '')] } else: # Editing existing build config itself config = BuildConfig.fetch(self.env, config_name) platforms = list( TargetPlatform.select(self.env, config=config.name)) if req.method == 'POST': if 'remove' in req.args: # Remove selected platforms self._remove_platforms(req) add_notice(req, "Target Platform(s) Removed.") req.redirect(req.abs_href.admin( cat, page, config.name)) elif 'save' in req.args: # Save this build config warnings = self._update_config(req, config) if not warnings: add_notice(req, "Configuration Saved.") req.redirect(req.abs_href.admin( cat, page, config.name)) for warning in warnings: add_warning(req, warning) # FIXME: Deprecation notice for old namespace. # Remove notice code when migration to new namespace is complete if 'http://bitten.cmlenz.net/tools/' in config.recipe: add_notice( req, "Recipe uses a deprecated namespace. " "Replace 'http://bitten.cmlenz.net/tools/' with " "'http://bitten.edgewall.org/tools/'.") # Add a notice if configuration is not active if not warnings and not config.active and config.recipe: add_notice( req, "Configuration is not active. Activate " "from main 'Configurations' listing to enable it.") # Prepare template variables data['config'] = { 'name': config.name, 'label': config.label or config.name, 'active': config.active, 'branch': config.branch, 'description': config.description, 'recipe': config.recipe, 'platforms': [{ 'name': platform.name, 'id': platform.id, 'href': req.href.admin('bitten', 'configs', config.name, platform.id), 'rules': [{ 'property': propname, 'pattern': pattern } for propname, pattern in platform.rules] } for platform in platforms] } else: # At the top level build config list if req.method == 'POST': if 'add' in req.args: # Add build config config = self._create_config(req) req.redirect(req.abs_href.admin(cat, page, config.name)) elif 'remove' in req.args: # Remove selected build configs self._remove_configs(req) elif 'apply' in req.args: # Update active state of configs self._activate_configs(req) req.redirect(req.abs_href.admin(cat, page)) # Prepare template variables configs = [] for config in BuildConfig.select(self.env, include_inactive=True): configs.append({ 'name': config.name, 'label': config.label or config.name, 'active': config.active, 'branch': config.branch, 'href': req.href.admin('bitten', 'configs', config.name), 'recipe': config.recipe and True or False }) data['configs'] = configs add_stylesheet(req, 'bitten/admin.css') add_script(req, 'common/js/suggest.js') return 'bitten_admin_configs.html', data