mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 15:35:46 +07:00
[IMP] runbot: refactor build error models
The initial idea to link an error to another one was a quick solution to group them if they where related, but this became challenging to copute metada regarding errors. - The displayed error message was not always consistent with the real root cause/the error that lead here. - The aggregates (lets says, linked buils ids) could be the one of the error, or from all error messages. Same for the versions, first seen, .. This is confusing to knwo what is the leist we are managing and what is the expecte result to display Main motivation: on a standard error page (will be changed to "assignment"), we want to have the list of error message that is related to this one. We want to know for each message (a real build error) what is the version, first seen, ... This will give more flexibility on the display, The assigned person/team/test-tags, ... are moved to this model The appearance data remains on the build error but are aggregate on the assignation.
This commit is contained in:
parent
d990b39258
commit
56e242a660
@ -6,7 +6,7 @@
|
|||||||
'author': "Odoo SA",
|
'author': "Odoo SA",
|
||||||
'website': "http://runbot.odoo.com",
|
'website': "http://runbot.odoo.com",
|
||||||
'category': 'Website',
|
'category': 'Website',
|
||||||
'version': '5.7',
|
'version': '5.8',
|
||||||
'application': True,
|
'application': True,
|
||||||
'depends': ['base', 'base_automation', 'website'],
|
'depends': ['base', 'base_automation', 'website'],
|
||||||
'data': [
|
'data': [
|
||||||
|
@ -30,7 +30,7 @@ def route(routes, **kw):
|
|||||||
keep_search = request.httprequest.cookies.get('keep_search', False) == '1'
|
keep_search = request.httprequest.cookies.get('keep_search', False) == '1'
|
||||||
cookie_search = request.httprequest.cookies.get('search', '')
|
cookie_search = request.httprequest.cookies.get('search', '')
|
||||||
refresh = kwargs.get('refresh', False)
|
refresh = kwargs.get('refresh', False)
|
||||||
nb_build_errors = request.env['runbot.build.error'].search_count([('random', '=', True), ('parent_id', '=', False)])
|
nb_build_errors = request.env['runbot.build.error'].search_count([])
|
||||||
nb_assigned_errors = request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)])
|
nb_assigned_errors = request.env['runbot.build.error'].search_count([('responsible', '=', request.env.user.id)])
|
||||||
nb_team_errors = request.env['runbot.build.error'].search_count([('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids)])
|
nb_team_errors = request.env['runbot.build.error'].search_count([('responsible', '=', False), ('team_id', 'in', request.env.user.runbot_team_ids.ids)])
|
||||||
kwargs['more'] = more
|
kwargs['more'] = more
|
||||||
@ -459,7 +459,7 @@ class Runbot(Controller):
|
|||||||
('responsible', '=', False),
|
('responsible', '=', False),
|
||||||
('team_id', 'in', request.env.user.runbot_team_ids.ids)
|
('team_id', 'in', request.env.user.runbot_team_ids.ids)
|
||||||
], order='last_seen_date desc, build_count desc')
|
], order='last_seen_date desc, build_count desc')
|
||||||
domain = [('parent_id', '=', False), ('responsible', '!=', request.env.user.id), ('build_count', '>', 1)]
|
domain = [('responsible', '!=', request.env.user.id), ('build_count', '>', 1)]
|
||||||
build_errors_count = request.env['runbot.build.error'].search_count(domain)
|
build_errors_count = request.env['runbot.build.error'].search_count(domain)
|
||||||
url_args = {}
|
url_args = {}
|
||||||
url_args['sort'] = sort
|
url_args['sort'] = sort
|
||||||
@ -481,7 +481,7 @@ class Runbot(Controller):
|
|||||||
@route(['/runbot/teams', '/runbot/teams/<model("runbot.team"):team>',], type='http', auth='user', website=True, sitemap=False)
|
@route(['/runbot/teams', '/runbot/teams/<model("runbot.team"):team>',], type='http', auth='user', website=True, sitemap=False)
|
||||||
def team_dashboards(self, team=None, hide_empty=False, **kwargs):
|
def team_dashboards(self, team=None, hide_empty=False, **kwargs):
|
||||||
teams = request.env['runbot.team'].search([]) if not team else None
|
teams = request.env['runbot.team'].search([]) if not team else None
|
||||||
domain = [('id', 'in', team.build_error_ids.ids)] if team else []
|
domain = [('id', 'in', team.assignment_ids.ids)] if team else []
|
||||||
|
|
||||||
# Sort & Filter
|
# Sort & Filter
|
||||||
sortby = kwargs.get('sortby', 'count')
|
sortby = kwargs.get('sortby', 'count')
|
||||||
@ -496,7 +496,7 @@ class Runbot(Controller):
|
|||||||
'not_one': {'label': 'Seen more than once', 'domain': [('build_count', '>', 1)]},
|
'not_one': {'label': 'Seen more than once', 'domain': [('build_count', '>', 1)]},
|
||||||
}
|
}
|
||||||
|
|
||||||
for trigger in team.build_error_ids.trigger_ids if team else []:
|
for trigger in team.assignment_ids.trigger_ids if team else []:
|
||||||
k = f'trigger_{trigger.name.lower().replace(" ", "_")}'
|
k = f'trigger_{trigger.name.lower().replace(" ", "_")}'
|
||||||
searchbar_filters.update(
|
searchbar_filters.update(
|
||||||
{k: {'label': f'Trigger {trigger.name}', 'domain': [('trigger_ids', '=', trigger.id)]}}
|
{k: {'label': f'Trigger {trigger.name}', 'domain': [('trigger_ids', '=', trigger.id)]}}
|
||||||
@ -510,7 +510,7 @@ class Runbot(Controller):
|
|||||||
qctx = {
|
qctx = {
|
||||||
'team': team,
|
'team': team,
|
||||||
'teams': teams,
|
'teams': teams,
|
||||||
'build_error_ids': request.env['runbot.build.error'].search(domain, order=order),
|
'build_assignment_ids': request.env['runbot.build.assignment'].search(domain, order=order),
|
||||||
'hide_empty': bool(hide_empty),
|
'hide_empty': bool(hide_empty),
|
||||||
'searchbar_sortings': searchbar_sortings,
|
'searchbar_sortings': searchbar_sortings,
|
||||||
'sortby': sortby,
|
'sortby': sortby,
|
||||||
|
@ -9,6 +9,16 @@
|
|||||||
records.action_link_errors()
|
records.action_link_errors()
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
<record model="ir.actions.server" id="action_link_build_errors_contents">
|
||||||
|
<field name="name">Link build errors contents</field>
|
||||||
|
<field name="model_id" ref="runbot.model_runbot_build_error" />
|
||||||
|
<field name="binding_model_id" ref="runbot.model_runbot_build_error" />
|
||||||
|
<field name="type">ir.actions.server</field>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">
|
||||||
|
records.action_link_errors_content()
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
<record model="ir.actions.server" id="action_clean_build_errors">
|
<record model="ir.actions.server" id="action_clean_build_errors">
|
||||||
<field name="name">Re-clean build errors</field>
|
<field name="name">Re-clean build errors</field>
|
||||||
<field name="model_id" ref="runbot.model_runbot_build_error" />
|
<field name="model_id" ref="runbot.model_runbot_build_error" />
|
||||||
|
123
runbot/migrations/17.0.5.8/post-migration.py
Normal file
123
runbot/migrations/17.0.5.8/post-migration.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(cr, version):
|
||||||
|
|
||||||
|
# get seen infos
|
||||||
|
cr.execute("SELECT error_content_id, min(build_id), min(log_date), max(build_id), max(log_date), count(DISTINCT build_id) FROM runbot_build_error_link GROUP BY error_content_id")
|
||||||
|
vals_by_error = {error: vals for error, *vals in cr.fetchall()}
|
||||||
|
|
||||||
|
# first_seen_build_id was not stored, lets fill it and update all values for good mesure
|
||||||
|
for error, vals in vals_by_error.items():
|
||||||
|
cr.execute('UPDATE runbot_build_error_content SET first_seen_build_id = %s, first_seen_date = %s, last_seen_build_id = %s, last_seen_date = %s WHERE id=%s', (vals[0], vals[1], vals[2], vals[3], error))
|
||||||
|
|
||||||
|
# generate flattened error hierarchy
|
||||||
|
cr.execute('''SELECT
|
||||||
|
id,
|
||||||
|
parent_id
|
||||||
|
FROM runbot_build_error_content
|
||||||
|
ORDER BY id
|
||||||
|
''')
|
||||||
|
|
||||||
|
error_by_parent = {}
|
||||||
|
for error_id, parent_id in cr.fetchall():
|
||||||
|
if parent_id:
|
||||||
|
error_by_parent.setdefault(parent_id, []).append(error_id)
|
||||||
|
stable = False
|
||||||
|
while not stable:
|
||||||
|
stable = True
|
||||||
|
for parent, child_ids in error_by_parent.items():
|
||||||
|
for child_id in child_ids:
|
||||||
|
if parent == child_id:
|
||||||
|
continue
|
||||||
|
sub_childrens = error_by_parent.get(child_id)
|
||||||
|
if sub_childrens:
|
||||||
|
error_by_parent[parent] = error_by_parent[parent] + sub_childrens
|
||||||
|
error_by_parent[child_id] = []
|
||||||
|
stable = False
|
||||||
|
for parent, child_ids in error_by_parent.items():
|
||||||
|
if parent in child_ids:
|
||||||
|
_logger.info('Breaking cycle parent on %s', parent)
|
||||||
|
error_by_parent[parent] = [c for c in child_ids if c != parent]
|
||||||
|
cr.execute('UPDATE runbot_build_error_content SET parent_id = null WHERE id=%s', (parent,))
|
||||||
|
error_by_parent = {parent: chilren for parent, chilren in error_by_parent.items() if chilren}
|
||||||
|
|
||||||
|
cr.execute('''SELECT
|
||||||
|
id,
|
||||||
|
active,
|
||||||
|
parent_id
|
||||||
|
random,
|
||||||
|
content,
|
||||||
|
test_tags,
|
||||||
|
tags_min_version_id,
|
||||||
|
tags_max_version_id,
|
||||||
|
team_id,
|
||||||
|
responsible,
|
||||||
|
customer,
|
||||||
|
fixing_commit,
|
||||||
|
fixing_pr_id
|
||||||
|
FROM runbot_build_error_content
|
||||||
|
WHERE parent_id IS null
|
||||||
|
ORDER BY id
|
||||||
|
''')
|
||||||
|
errors = cr.fetchall()
|
||||||
|
nb_groups = len(error_by_parent)
|
||||||
|
_logger.info('Creating %s errors', nb_groups)
|
||||||
|
for error in errors:
|
||||||
|
error_id, *values = error
|
||||||
|
children = error_by_parent.get(error_id, [])
|
||||||
|
assert not error_id in children
|
||||||
|
all_errors = [error_id, *children]
|
||||||
|
error_count = len(all_errors)
|
||||||
|
|
||||||
|
# vals_by_error order: min(build_id), min(log_date), max(build_id), max(log_date)
|
||||||
|
build_count = 0
|
||||||
|
first_seen_build_id = first_seen_date = last_seen_build_id = last_seen_date = None
|
||||||
|
if error_id in vals_by_error:
|
||||||
|
error_vals = [vals_by_error[error_id] for error_id in all_errors]
|
||||||
|
first_seen_build_id = min(vals[0] for vals in error_vals)
|
||||||
|
first_seen_date = min(vals[1] for vals in error_vals)
|
||||||
|
last_seen_build_id = max(vals[2] for vals in error_vals)
|
||||||
|
last_seen_date = max(vals[3] for vals in error_vals)
|
||||||
|
build_count = sum(vals[4] for vals in error_vals) # not correct for distinct but close enough
|
||||||
|
assert first_seen_date <= last_seen_date
|
||||||
|
assert first_seen_build_id <= last_seen_build_id
|
||||||
|
name = values[2].split('\n')[0]
|
||||||
|
|
||||||
|
values = [error_id, *values, last_seen_build_id, first_seen_build_id, last_seen_date, first_seen_date, build_count, error_count, name]
|
||||||
|
|
||||||
|
cr.execute('''
|
||||||
|
INSERT INTO runbot_build_error (
|
||||||
|
id,
|
||||||
|
active,
|
||||||
|
random,
|
||||||
|
description,
|
||||||
|
test_tags,
|
||||||
|
tags_min_version_id,
|
||||||
|
tags_max_version_id,
|
||||||
|
team_id,
|
||||||
|
responsible,
|
||||||
|
customer,
|
||||||
|
fixing_commit,
|
||||||
|
fixing_pr_id,
|
||||||
|
last_seen_build_id,
|
||||||
|
first_seen_build_id,
|
||||||
|
last_seen_date,
|
||||||
|
first_seen_date,
|
||||||
|
build_count,
|
||||||
|
error_count,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
VALUES (%s)
|
||||||
|
RETURNING id
|
||||||
|
''' % ', '.join(['%s'] * len(values)), values) # noqa: S608
|
||||||
|
|
||||||
|
error_id = cr.fetchone()
|
||||||
|
cr.execute('UPDATE runbot_build_error_content SET error_id = %s WHERE id in %s', (error_id, tuple(all_errors)))
|
||||||
|
|
||||||
|
cr.execute('ALTER TABLE runbot_build_error_content ALTER COLUMN error_id SET NOT NULL')
|
||||||
|
cr.execute('SELECT max(id) from runbot_build_error')
|
||||||
|
cr.execute("SELECT SETVAL('runbot_build_error_id_seq', %s)", (cr.fetchone()[0] + 1,))
|
||||||
|
_logger.info('Done')
|
4
runbot/migrations/17.0.5.8/pre-migration.py
Normal file
4
runbot/migrations/17.0.5.8/pre-migration.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
def migrate(cr, version):
|
||||||
|
cr.execute('ALTER TABLE runbot_build_error RENAME TO runbot_build_error_content')
|
||||||
|
cr.execute('ALTER TABLE runbot_build_error_content ADD COLUMN first_seen_build_id INT')
|
||||||
|
cr.execute('ALTER TABLE runbot_build_error_link RENAME COLUMN build_error_id TO error_content_id')
|
@ -237,6 +237,7 @@ class Batch(models.Model):
|
|||||||
# use last not preparing batch to define previous repos_heads instead of branches heads:
|
# use last not preparing batch to define previous repos_heads instead of branches heads:
|
||||||
# Will allow to have a diff info on base bundle, compare with previous bundle
|
# Will allow to have a diff info on base bundle, compare with previous bundle
|
||||||
last_base_batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.base_id.id), ('state', '!=', 'preparing'), ('category_id', '=', self.category_id.id), ('id', '!=', self.id)], order='id desc', limit=1)
|
last_base_batch = self.env['runbot.batch'].search([('bundle_id', '=', bundle.base_id.id), ('state', '!=', 'preparing'), ('category_id', '=', self.category_id.id), ('id', '!=', self.id)], order='id desc', limit=1)
|
||||||
|
if last_base_batch:
|
||||||
base_head_per_repo = {commit.repo_id.id: commit for commit in last_base_batch.commit_ids}
|
base_head_per_repo = {commit.repo_id.id: commit for commit in last_base_batch.commit_ids}
|
||||||
self._update_commits_infos(base_head_per_repo) # set base_commit, diff infos, ...
|
self._update_commits_infos(base_head_per_repo) # set base_commit, diff infos, ...
|
||||||
|
|
||||||
@ -496,7 +497,6 @@ class BatchSlot(models.Model):
|
|||||||
_description = 'Link between a bundle batch and a build'
|
_description = 'Link between a bundle batch and a build'
|
||||||
_order = 'trigger_id,id'
|
_order = 'trigger_id,id'
|
||||||
|
|
||||||
|
|
||||||
batch_id = fields.Many2one('runbot.batch', index=True)
|
batch_id = fields.Many2one('runbot.batch', index=True)
|
||||||
trigger_id = fields.Many2one('runbot.trigger', index=True)
|
trigger_id = fields.Many2one('runbot.trigger', index=True)
|
||||||
build_id = fields.Many2one('runbot.build', index=True)
|
build_id = fields.Many2one('runbot.build', index=True)
|
||||||
|
@ -316,7 +316,7 @@ class BuildResult(models.Model):
|
|||||||
@api.depends('build_error_link_ids')
|
@api.depends('build_error_link_ids')
|
||||||
def _compute_build_error_ids(self):
|
def _compute_build_error_ids(self):
|
||||||
for record in self:
|
for record in self:
|
||||||
record.build_error_ids = record.build_error_link_ids.mapped('build_error_id')
|
record.build_error_ids = record.build_error_link_ids.error_content_id.error_id
|
||||||
|
|
||||||
def _get_worst_result(self, results, max_res=False):
|
def _get_worst_result(self, results, max_res=False):
|
||||||
results = [result for result in results if result] # filter Falsy values
|
results = [result for result in results if result] # filter Falsy values
|
||||||
@ -1182,11 +1182,10 @@ class BuildResult(models.Model):
|
|||||||
|
|
||||||
def _parse_logs(self):
|
def _parse_logs(self):
|
||||||
""" Parse build logs to classify errors """
|
""" Parse build logs to classify errors """
|
||||||
BuildError = self.env['runbot.build.error']
|
|
||||||
# only parse logs from builds in error and not already scanned
|
# only parse logs from builds in error and not already scanned
|
||||||
builds_to_scan = self.search([('id', 'in', self.ids), ('local_result', 'in', ('ko', 'killed', 'warn')), ('build_error_link_ids', '=', False)])
|
builds_to_scan = self.search([('id', 'in', self.ids), ('local_result', 'in', ('ko', 'killed', 'warn')), ('build_error_link_ids', '=', False)])
|
||||||
ir_logs = self.env['ir.logging'].search([('level', 'in', ('ERROR', 'WARNING', 'CRITICAL')), ('type', '=', 'server'), ('build_id', 'in', builds_to_scan.ids)])
|
ir_logs = self.env['ir.logging'].search([('level', 'in', ('ERROR', 'WARNING', 'CRITICAL')), ('type', '=', 'server'), ('build_id', 'in', builds_to_scan.ids)])
|
||||||
return BuildError._parse_logs(ir_logs)
|
return self.env['runbot.build.error']._parse_logs(ir_logs)
|
||||||
|
|
||||||
def _is_file(self, file, mode='r'):
|
def _is_file(self, file, mode='r'):
|
||||||
file_path = self._path(file)
|
file_path = self._path(file)
|
||||||
|
@ -19,7 +19,7 @@ class BuildErrorLink(models.Model):
|
|||||||
_order = 'log_date desc, build_id desc'
|
_order = 'log_date desc, build_id desc'
|
||||||
|
|
||||||
build_id = fields.Many2one('runbot.build', required=True, index=True)
|
build_id = fields.Many2one('runbot.build', required=True, index=True)
|
||||||
build_error_id =fields.Many2one('runbot.build.error', required=True, index=True, ondelete='cascade')
|
error_content_id = fields.Many2one('runbot.build.error.content', required=True, index=True, ondelete='cascade')
|
||||||
log_date = fields.Datetime(string='Log date')
|
log_date = fields.Datetime(string='Log date')
|
||||||
host = fields.Char(related='build_id.host')
|
host = fields.Char(related='build_id.host')
|
||||||
dest = fields.Char(related='build_id.dest')
|
dest = fields.Char(related='build_id.dest')
|
||||||
@ -29,26 +29,58 @@ class BuildErrorLink(models.Model):
|
|||||||
build_url = fields.Char(related='build_id.build_url')
|
build_url = fields.Char(related='build_id.build_url')
|
||||||
|
|
||||||
_sql_constraints = [
|
_sql_constraints = [
|
||||||
('error_build_rel_unique', 'UNIQUE (build_id, build_error_id)', 'A link between a build and an error must be unique'),
|
('error_build_rel_unique', 'UNIQUE (build_id, error_content_id)', 'A link between a build and an error must be unique'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BuildErrorSeenMixin(models.AbstractModel):
|
||||||
|
_name = 'runbot.build.error.seen.mixin'
|
||||||
|
_description = "Add last/firt build/log_date for error and asssignments"
|
||||||
|
|
||||||
|
first_seen_build_id = fields.Many2one('runbot.build', compute='_compute_seen', string='First Seen build', store=True)
|
||||||
|
first_seen_date = fields.Datetime(string='First Seen Date', compute='_compute_seen', store=True)
|
||||||
|
last_seen_build_id = fields.Many2one('runbot.build', compute='_compute_seen', string='Last Seen build', store=True)
|
||||||
|
last_seen_date = fields.Datetime(string='Last Seen Date', compute='_compute_seen', store=True)
|
||||||
|
build_count = fields.Integer(string='Nb Seen', compute='_compute_seen', store=True)
|
||||||
|
|
||||||
|
@api.depends('build_error_link_ids')
|
||||||
|
def _compute_seen(self):
|
||||||
|
for record in self:
|
||||||
|
record.first_seen_date = False
|
||||||
|
record.last_seen_date = False
|
||||||
|
record.build_count = 0
|
||||||
|
error_link_ids = record.build_error_link_ids.sorted('log_date')
|
||||||
|
if error_link_ids:
|
||||||
|
first_error_link = error_link_ids[0]
|
||||||
|
last_error_link = error_link_ids[-1]
|
||||||
|
record.first_seen_date = first_error_link.log_date
|
||||||
|
record.last_seen_date = last_error_link.log_date
|
||||||
|
record.first_seen_build_id = first_error_link.build_id
|
||||||
|
record.last_seen_build_id = last_error_link.build_id
|
||||||
|
record.build_count = len(error_link_ids.build_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_related_error_content_ids(field_name):
|
||||||
|
@api.depends(f'error_content_ids.{field_name}')
|
||||||
|
def _compute(self):
|
||||||
|
for record in self:
|
||||||
|
record[field_name] = record.error_content_ids[field_name]
|
||||||
|
return _compute
|
||||||
|
|
||||||
|
|
||||||
class BuildError(models.Model):
|
class BuildError(models.Model):
|
||||||
|
|
||||||
_name = "runbot.build.error"
|
_name = "runbot.build.error"
|
||||||
_description = "Build error"
|
_description = "An object to manage a group of errors log that fit together and assign them to a team"
|
||||||
|
_inherit = ('mail.thread', 'mail.activity.mixin', 'runbot.build.error.seen.mixin')
|
||||||
|
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
name = fields.Char("Name")
|
||||||
_rec_name = "id"
|
active = fields.Boolean('Open (not fixed)', default=True, tracking=True)
|
||||||
|
description = fields.Text("Description", store=True, compute='_compute_description')
|
||||||
|
content = fields.Text("Error contents", compute='_compute_content', search="_search_content")
|
||||||
|
error_content_ids = fields.One2many('runbot.build.error.content', 'error_id')
|
||||||
|
error_count = fields.Integer("Error count", store=True, compute='_compute_count')
|
||||||
|
previous_error_id = fields.Many2one('runbot.build.error', string="Already seen error")
|
||||||
|
|
||||||
content = fields.Text('Error message', required=True)
|
|
||||||
cleaned_content = fields.Text('Cleaned error message')
|
|
||||||
summary = fields.Char('Content summary', compute='_compute_summary', store=False)
|
|
||||||
module_name = fields.Char('Module name') # name in ir_logging
|
|
||||||
file_path = fields.Char('File Path') # path in ir logging
|
|
||||||
function = fields.Char('Function name') # func name in ir logging
|
|
||||||
fingerprint = fields.Char('Error fingerprint', index=True)
|
|
||||||
random = fields.Boolean('underterministic error', tracking=True)
|
|
||||||
responsible = fields.Many2one('res.users', 'Assigned fixer', tracking=True)
|
responsible = fields.Many2one('res.users', 'Assigned fixer', tracking=True)
|
||||||
customer = fields.Many2one('res.users', 'Customer', tracking=True)
|
customer = fields.Many2one('res.users', 'Customer', tracking=True)
|
||||||
team_id = fields.Many2one('runbot.team', 'Assigned team', tracking=True)
|
team_id = fields.Many2one('runbot.team', 'Assigned team', tracking=True)
|
||||||
@ -56,27 +88,58 @@ class BuildError(models.Model):
|
|||||||
fixing_pr_id = fields.Many2one('runbot.branch', 'Fixing PR', tracking=True, domain=[('is_pr', '=', True)])
|
fixing_pr_id = fields.Many2one('runbot.branch', 'Fixing PR', tracking=True, domain=[('is_pr', '=', True)])
|
||||||
fixing_pr_alive = fields.Boolean('Fixing PR alive', related='fixing_pr_id.alive')
|
fixing_pr_alive = fields.Boolean('Fixing PR alive', related='fixing_pr_id.alive')
|
||||||
fixing_pr_url = fields.Char('Fixing PR url', related='fixing_pr_id.branch_url')
|
fixing_pr_url = fields.Char('Fixing PR url', related='fixing_pr_id.branch_url')
|
||||||
build_error_link_ids = fields.One2many('runbot.build.error.link', 'build_error_id')
|
|
||||||
children_build_error_link_ids = fields.One2many('runbot.build.error.link', compute='_compute_children_build_error_link_ids')
|
|
||||||
build_ids = fields.Many2many('runbot.build', compute= '_compute_build_ids')
|
|
||||||
bundle_ids = fields.One2many('runbot.bundle', compute='_compute_bundle_ids')
|
|
||||||
version_ids = fields.One2many('runbot.version', compute='_compute_version_ids', string='Versions', search='_search_version')
|
|
||||||
trigger_ids = fields.Many2many('runbot.trigger', compute='_compute_trigger_ids', string='Triggers', search='_search_trigger_ids')
|
|
||||||
active = fields.Boolean('Active (not fixed)', default=True, tracking=True)
|
|
||||||
tag_ids = fields.Many2many('runbot.build.error.tag', string='Tags')
|
|
||||||
build_count = fields.Integer(compute='_compute_build_counts', string='Nb seen', store=True)
|
|
||||||
parent_id = fields.Many2one('runbot.build.error', 'Linked to', index=True)
|
|
||||||
child_ids = fields.One2many('runbot.build.error', 'parent_id', string='Child Errors', context={'active_test': False})
|
|
||||||
children_build_ids = fields.Many2many('runbot.build', compute='_compute_children_build_ids', string='Children builds')
|
|
||||||
error_history_ids = fields.Many2many('runbot.build.error', compute='_compute_error_history_ids', string='Old errors', context={'active_test': False})
|
|
||||||
first_seen_build_id = fields.Many2one('runbot.build', compute='_compute_first_seen_build_id', string='First Seen build')
|
|
||||||
first_seen_date = fields.Datetime(string='First Seen Date', compute='_compute_seen_date', store=True)
|
|
||||||
last_seen_build_id = fields.Many2one('runbot.build', compute='_compute_last_seen_build_id', string='Last Seen build', store=True)
|
|
||||||
last_seen_date = fields.Datetime(string='Last Seen Date', compute='_compute_seen_date', store=True)
|
|
||||||
test_tags = fields.Char(string='Test tags', help="Comma separated list of test_tags to use to reproduce/remove this error", tracking=True)
|
test_tags = fields.Char(string='Test tags', help="Comma separated list of test_tags to use to reproduce/remove this error", tracking=True)
|
||||||
tags_min_version_id = fields.Many2one('runbot.version', 'Tags Min version', help="Minimal version where the test tags will be applied.")
|
tags_min_version_id = fields.Many2one('runbot.version', 'Tags Min version', help="Minimal version where the test tags will be applied.")
|
||||||
tags_max_version_id = fields.Many2one('runbot.version', 'Tags Max version', help="Maximal version where the test tags will be applied.")
|
tags_max_version_id = fields.Many2one('runbot.version', 'Tags Max version', help="Maximal version where the test tags will be applied.")
|
||||||
|
|
||||||
|
# Build error related data
|
||||||
|
build_error_link_ids = fields.Many2many('runbot.build.error.link', compute=_compute_related_error_content_ids('build_error_link_ids'))
|
||||||
|
unique_build_error_link_ids = fields.Many2many('runbot.build.error.link', compute='_compute_unique_build_error_link_ids')
|
||||||
|
build_ids = fields.Many2many('runbot.build', compute=_compute_related_error_content_ids('build_ids'))
|
||||||
|
bundle_ids = fields.Many2many('runbot.bundle', compute=_compute_related_error_content_ids('bundle_ids'))
|
||||||
|
version_ids = fields.Many2many('runbot.version', string='Versions', compute=_compute_related_error_content_ids('version_ids'))
|
||||||
|
trigger_ids = fields.Many2many('runbot.trigger', string='Triggers', compute=_compute_related_error_content_ids('trigger_ids'))
|
||||||
|
tag_ids = fields.Many2many('runbot.build.error.tag', string='Tags', compute=_compute_related_error_content_ids('tag_ids'))
|
||||||
|
|
||||||
|
random = fields.Boolean('Random', compute="_compute_random", store=True)
|
||||||
|
|
||||||
|
@api.depends('build_error_link_ids')
|
||||||
|
def _compute_unique_build_error_link_ids(self):
|
||||||
|
for record in self:
|
||||||
|
seen = set()
|
||||||
|
id_list = []
|
||||||
|
for error_link in record.build_error_link_ids:
|
||||||
|
if error_link.build_id.id not in seen:
|
||||||
|
seen.add(error_link.build_id.id)
|
||||||
|
id_list.append(error_link.id)
|
||||||
|
record.unique_build_error_link_ids = record.env['runbot.build.error.link'].browse(id_list)
|
||||||
|
|
||||||
|
@api.depends('name', 'error_content_ids')
|
||||||
|
def _compute_description(self):
|
||||||
|
for record in self:
|
||||||
|
record.description = record.name
|
||||||
|
if record.error_content_ids:
|
||||||
|
record.description = record.error_content_ids[0].content
|
||||||
|
|
||||||
|
def _compute_content(self):
|
||||||
|
for record in self:
|
||||||
|
record.content = '\n'.join(record.error_content_ids.mapped('content'))
|
||||||
|
|
||||||
|
def _search_content(self, operator, value):
|
||||||
|
return [('error_content_ids', 'any', [('content', operator, value)])]
|
||||||
|
|
||||||
|
@api.depends('error_content_ids')
|
||||||
|
def _compute_count(self):
|
||||||
|
for record in self:
|
||||||
|
record.error_count = len(record.error_content_ids)
|
||||||
|
|
||||||
|
@api.depends('error_content_ids')
|
||||||
|
def _compute_random(self):
|
||||||
|
for record in self:
|
||||||
|
record.random = any(error.random for error in record.error_content_ids)
|
||||||
|
|
||||||
|
|
||||||
@api.constrains('test_tags')
|
@api.constrains('test_tags')
|
||||||
def _check_test_tags(self):
|
def _check_test_tags(self):
|
||||||
for build_error in self:
|
for build_error in self:
|
||||||
@ -85,6 +148,7 @@ class BuildError(models.Model):
|
|||||||
|
|
||||||
@api.onchange('test_tags')
|
@api.onchange('test_tags')
|
||||||
def _onchange_test_tags(self):
|
def _onchange_test_tags(self):
|
||||||
|
if self.test_tags and self.version_ids:
|
||||||
self.tags_min_version_id = min(self.version_ids, key=lambda rec: rec.number)
|
self.tags_min_version_id = min(self.version_ids, key=lambda rec: rec.number)
|
||||||
self.tags_max_version_id = max(self.version_ids, key=lambda rec: rec.number)
|
self.tags_max_version_id = max(self.version_ids, key=lambda rec: rec.number)
|
||||||
|
|
||||||
@ -93,16 +157,7 @@ class BuildError(models.Model):
|
|||||||
if not self.responsible:
|
if not self.responsible:
|
||||||
self.responsible = self.customer
|
self.responsible = self.customer
|
||||||
|
|
||||||
@api.model_create_multi
|
|
||||||
def create(self, vals_list):
|
def create(self, vals_list):
|
||||||
cleaners = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
|
||||||
for vals in vals_list:
|
|
||||||
content = vals.get('content')
|
|
||||||
cleaned_content = cleaners._r_sub(content)
|
|
||||||
vals.update({
|
|
||||||
'cleaned_content': cleaned_content,
|
|
||||||
'fingerprint': self._digest(cleaned_content)
|
|
||||||
})
|
|
||||||
records = super().create(vals_list)
|
records = super().create(vals_list)
|
||||||
records.action_assign()
|
records.action_assign()
|
||||||
return records
|
return records
|
||||||
@ -110,170 +165,35 @@ class BuildError(models.Model):
|
|||||||
def write(self, vals):
|
def write(self, vals):
|
||||||
if 'active' in vals:
|
if 'active' in vals:
|
||||||
for build_error in self:
|
for build_error in self:
|
||||||
(build_error.child_ids - self).write({'active': vals['active']})
|
|
||||||
if not (self.env.su or self.user_has_groups('runbot.group_runbot_admin')):
|
if not (self.env.su or self.user_has_groups('runbot.group_runbot_admin')):
|
||||||
if build_error.test_tags:
|
if build_error.test_tags:
|
||||||
raise UserError("This error as a test-tag and can only be (de)activated by admin")
|
raise UserError("This error as a test-tag and can only be (de)activated by admin")
|
||||||
if not vals['active'] and build_error.last_seen_date + relativedelta(days=1) > fields.Datetime.now():
|
if not vals['active'] and build_error.last_seen_date + relativedelta(days=1) > fields.Datetime.now():
|
||||||
raise UserError("This error broke less than one day ago can only be deactivated by admin")
|
raise UserError("This error broke less than one day ago can only be deactivated by admin")
|
||||||
if 'cleaned_content' in vals:
|
return super().write(vals)
|
||||||
vals.update({'fingerprint': self._digest(vals['cleaned_content'])})
|
|
||||||
result = super(BuildError, self).write(vals)
|
def _merge(self, others):
|
||||||
if vals.get('parent_id'):
|
self.ensure_one
|
||||||
for build_error in self:
|
error = self
|
||||||
parent = build_error.parent_id
|
for previous_error in others:
|
||||||
if build_error.test_tags:
|
# todo, check that all relevant fields are checked and transfered/logged
|
||||||
if parent.test_tags and not self.env.su:
|
if previous_error.test_tags and error.test_tags != previous_error.test_tags:
|
||||||
raise UserError(f"Cannot parent an error with test tags: {build_error.test_tags}")
|
if previous_error.test_tags and not self.env.su:
|
||||||
elif not parent.test_tags:
|
raise UserError(f"Cannot merge an error with test tags: {previous_error.test_tags}")
|
||||||
parent.sudo().test_tags = build_error.test_tags
|
elif not error.test_tags:
|
||||||
build_error.sudo().test_tags = False
|
error.sudo().test_tags = previous_error.test_tags
|
||||||
if build_error.responsible:
|
previous_error.sudo().test_tags = False
|
||||||
if parent.responsible and parent.responsible != build_error.responsible and not self.env.su:
|
if previous_error.responsible:
|
||||||
raise UserError(f"Error {parent.id} as already a responsible ({parent.responsible}) cannot assign {build_error.responsible}")
|
if error.responsible and error.responsible != previous_error.responsible and not self.env.su:
|
||||||
|
raise UserError(f"error {error.id} as already a responsible ({error.responsible}) cannot assign {previous_error.responsible}")
|
||||||
else:
|
else:
|
||||||
parent.responsible = build_error.responsible
|
error.responsible = previous_error.responsible
|
||||||
build_error.responsible = False
|
if previous_error.team_id:
|
||||||
if build_error.team_id:
|
if not error.team_id:
|
||||||
if not parent.team_id:
|
error.team_id = previous_error.team_id
|
||||||
parent.team_id = build_error.team_id
|
previous_error.error_content_ids.write({'error_id': self})
|
||||||
build_error.team_id = False
|
if not previous_error.test_tags:
|
||||||
return result
|
previous_error.active = False
|
||||||
|
|
||||||
@api.depends('build_error_link_ids')
|
|
||||||
def _compute_build_ids(self):
|
|
||||||
for record in self:
|
|
||||||
record.build_ids = record.build_error_link_ids.mapped('build_id')
|
|
||||||
|
|
||||||
@api.depends('build_error_link_ids')
|
|
||||||
def _compute_children_build_error_link_ids(self):
|
|
||||||
for record in self:
|
|
||||||
record.children_build_error_link_ids = record.build_error_link_ids | record.child_ids.build_error_link_ids
|
|
||||||
|
|
||||||
@api.depends('build_ids', 'child_ids.build_ids')
|
|
||||||
def _compute_build_counts(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.build_count = len(build_error.build_ids | build_error.mapped('child_ids.build_ids'))
|
|
||||||
|
|
||||||
@api.depends('build_ids')
|
|
||||||
def _compute_bundle_ids(self):
|
|
||||||
for build_error in self:
|
|
||||||
top_parent_builds = build_error.build_ids.mapped(lambda rec: rec and rec.top_parent)
|
|
||||||
build_error.bundle_ids = top_parent_builds.mapped('slot_ids').mapped('batch_id.bundle_id')
|
|
||||||
|
|
||||||
@api.depends('children_build_ids')
|
|
||||||
def _compute_version_ids(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.version_ids = build_error.children_build_ids.version_id
|
|
||||||
|
|
||||||
@api.depends('children_build_ids')
|
|
||||||
def _compute_trigger_ids(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.trigger_ids = build_error.children_build_ids.trigger_id
|
|
||||||
|
|
||||||
@api.depends('content')
|
|
||||||
def _compute_summary(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.summary = build_error.content[:80]
|
|
||||||
|
|
||||||
|
|
||||||
@api.depends('build_ids', 'child_ids.build_ids')
|
|
||||||
def _compute_children_build_ids(self):
|
|
||||||
for build_error in self:
|
|
||||||
all_builds = build_error.build_ids | build_error.mapped('child_ids.build_ids')
|
|
||||||
build_error.children_build_ids = all_builds.sorted(key=lambda rec: rec.id, reverse=True)
|
|
||||||
|
|
||||||
@api.depends('children_build_ids')
|
|
||||||
def _compute_last_seen_build_id(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.last_seen_build_id = build_error.children_build_ids and build_error.children_build_ids[0] or False
|
|
||||||
|
|
||||||
@api.depends('build_error_link_ids', 'child_ids.build_error_link_ids')
|
|
||||||
def _compute_seen_date(self):
|
|
||||||
for build_error in self:
|
|
||||||
error_dates = (build_error.build_error_link_ids | build_error.child_ids.build_error_link_ids).mapped('log_date')
|
|
||||||
build_error.first_seen_date = error_dates and min(error_dates)
|
|
||||||
build_error.last_seen_date = error_dates and max(error_dates)
|
|
||||||
|
|
||||||
@api.depends('children_build_ids')
|
|
||||||
def _compute_first_seen_build_id(self):
|
|
||||||
for build_error in self:
|
|
||||||
build_error.first_seen_build_id = build_error.children_build_ids and build_error.children_build_ids[-1] or False
|
|
||||||
|
|
||||||
@api.depends('fingerprint', 'child_ids.fingerprint')
|
|
||||||
def _compute_error_history_ids(self):
|
|
||||||
for error in self:
|
|
||||||
fingerprints = [error.fingerprint] + [rec.fingerprint for rec in error.child_ids]
|
|
||||||
error.error_history_ids = self.search([('fingerprint', 'in', fingerprints), ('active', '=', False), ('id', '!=', error.id or False)])
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _digest(self, s):
|
|
||||||
"""
|
|
||||||
return a hash 256 digest of the string s
|
|
||||||
"""
|
|
||||||
return hashlib.sha256(s.encode()).hexdigest()
|
|
||||||
|
|
||||||
@api.model
|
|
||||||
def _parse_logs(self, ir_logs):
|
|
||||||
if not ir_logs:
|
|
||||||
return
|
|
||||||
regexes = self.env['runbot.error.regex'].search([])
|
|
||||||
search_regs = regexes.filtered(lambda r: r.re_type == 'filter')
|
|
||||||
cleaning_regs = regexes.filtered(lambda r: r.re_type == 'cleaning')
|
|
||||||
|
|
||||||
hash_dict = defaultdict(self.env['ir.logging'].browse)
|
|
||||||
for log in ir_logs:
|
|
||||||
if search_regs._r_search(log.message):
|
|
||||||
continue
|
|
||||||
fingerprint = self._digest(cleaning_regs._r_sub(log.message))
|
|
||||||
hash_dict[fingerprint] |= log
|
|
||||||
|
|
||||||
build_errors = self.env['runbot.build.error']
|
|
||||||
# add build ids to already detected errors
|
|
||||||
existing_errors = self.env['runbot.build.error'].search([('fingerprint', 'in', list(hash_dict.keys())), ('active', '=', True)])
|
|
||||||
existing_fingerprints = existing_errors.mapped('fingerprint')
|
|
||||||
build_errors |= existing_errors
|
|
||||||
for build_error in existing_errors:
|
|
||||||
logs = hash_dict[build_error.fingerprint]
|
|
||||||
# update filepath if it changed. This is optionnal and mainly there in case we adapt the OdooRunner log
|
|
||||||
if logs[0].path != build_error.file_path:
|
|
||||||
build_error.file_path = logs[0].path
|
|
||||||
build_error.function = logs[0].func
|
|
||||||
|
|
||||||
# create an error for the remaining entries
|
|
||||||
for fingerprint, logs in hash_dict.items():
|
|
||||||
if fingerprint in existing_fingerprints:
|
|
||||||
continue
|
|
||||||
new_build_error = self.env['runbot.build.error'].create({
|
|
||||||
'content': logs[0].message,
|
|
||||||
'module_name': logs[0].name.removeprefix('odoo.').removeprefix('addons.'),
|
|
||||||
'file_path': logs[0].path,
|
|
||||||
'function': logs[0].func,
|
|
||||||
})
|
|
||||||
build_errors |= new_build_error
|
|
||||||
existing_fingerprints.append(fingerprint)
|
|
||||||
|
|
||||||
for build_error in build_errors:
|
|
||||||
logs = hash_dict[build_error.fingerprint]
|
|
||||||
for rec in logs:
|
|
||||||
if rec.build_id not in build_error.build_error_link_ids.build_id:
|
|
||||||
self.env['runbot.build.error.link'].create({
|
|
||||||
'build_id': rec.build_id.id,
|
|
||||||
'build_error_id': build_error.id,
|
|
||||||
'log_date': rec.create_date
|
|
||||||
})
|
|
||||||
|
|
||||||
if build_errors:
|
|
||||||
window_action = {
|
|
||||||
"type": "ir.actions.act_window",
|
|
||||||
"res_model": "runbot.build.error",
|
|
||||||
"views": [[False, "tree"]],
|
|
||||||
"domain": [('id', 'in', build_errors.ids)]
|
|
||||||
}
|
|
||||||
if len(build_errors) == 1:
|
|
||||||
window_action["views"] = [[False, "form"]]
|
|
||||||
window_action["res_id"] = build_errors.id
|
|
||||||
return window_action
|
|
||||||
|
|
||||||
@api.model
|
@api.model
|
||||||
def _test_tags_list(self, build_id=False):
|
def _test_tags_list(self, build_id=False):
|
||||||
@ -293,6 +213,224 @@ class BuildError(models.Model):
|
|||||||
def _disabling_tags(self, build_id=False):
|
def _disabling_tags(self, build_id=False):
|
||||||
return ['-%s' % tag for tag in self._test_tags_list(build_id)]
|
return ['-%s' % tag for tag in self._test_tags_list(build_id)]
|
||||||
|
|
||||||
|
def _get_form_url(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return url_join(self.get_base_url(), f'/web#id={self.id}&model=runbot.build.error&view_type=form')
|
||||||
|
|
||||||
|
def _get_form_link(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return Markup('<a href="%s">%s</a>') % (self._get_form_url(), self.id)
|
||||||
|
|
||||||
|
def action_view_errors(self):
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'views': [(False, 'tree'), (False, 'form')],
|
||||||
|
'res_model': 'runbot.build.error.content',
|
||||||
|
'domain': [('error_id', '=', self.id)],
|
||||||
|
'context': {'active_test': False},
|
||||||
|
'target': 'current',
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_assign(self):
|
||||||
|
teams = None
|
||||||
|
repos = None
|
||||||
|
for record in self:
|
||||||
|
if not record.responsible and not record.team_id:
|
||||||
|
for error_content in record.error_content_ids:
|
||||||
|
if error_content.file_path:
|
||||||
|
if teams is None:
|
||||||
|
teams = self.env['runbot.team'].search(['|', ('path_glob', '!=', False), ('module_ownership_ids', '!=', False)])
|
||||||
|
repos = self.env['runbot.repo'].search([])
|
||||||
|
team = teams._get_team(error_content.file_path, repos)
|
||||||
|
if team:
|
||||||
|
record.team_id = team
|
||||||
|
break
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _parse_logs(self, ir_logs):
|
||||||
|
if not ir_logs:
|
||||||
|
return
|
||||||
|
regexes = self.env['runbot.error.regex'].search([])
|
||||||
|
search_regs = regexes.filtered(lambda r: r.re_type == 'filter')
|
||||||
|
cleaning_regs = regexes.filtered(lambda r: r.re_type == 'cleaning')
|
||||||
|
|
||||||
|
hash_dict = defaultdict(self.env['ir.logging'].browse)
|
||||||
|
for log in ir_logs:
|
||||||
|
if search_regs._r_search(log.message):
|
||||||
|
continue
|
||||||
|
fingerprint = self.env['runbot.build.error.content']._digest(cleaning_regs._r_sub(log.message))
|
||||||
|
hash_dict[fingerprint] |= log
|
||||||
|
|
||||||
|
build_error_contents = self.env['runbot.build.error.content']
|
||||||
|
# add build ids to already detected errors
|
||||||
|
existing_errors_contents = self.env['runbot.build.error.content'].search([('fingerprint', 'in', list(hash_dict.keys())), ('error_id.active', '=', True)])
|
||||||
|
existing_fingerprints = existing_errors_contents.mapped('fingerprint')
|
||||||
|
build_error_contents |= existing_errors_contents
|
||||||
|
# for build_error_content in existing_errors_contents:
|
||||||
|
# logs = hash_dict[build_error_content.fingerprint]
|
||||||
|
# # update filepath if it changed. This is optionnal and mainly there in case we adapt the OdooRunner log
|
||||||
|
# if logs[0].path != build_error_content.file_path:
|
||||||
|
# build_error_content.file_path = logs[0].path
|
||||||
|
# build_error_content.function = logs[0].func
|
||||||
|
|
||||||
|
# create an error for the remaining entries
|
||||||
|
for fingerprint, logs in hash_dict.items():
|
||||||
|
if fingerprint in existing_fingerprints:
|
||||||
|
continue
|
||||||
|
new_build_error_content = self.env['runbot.build.error.content'].create({
|
||||||
|
'content': logs[0].message,
|
||||||
|
'module_name': logs[0].name.removeprefix('odoo.').removeprefix('addons.'),
|
||||||
|
'file_path': logs[0].path,
|
||||||
|
'function': logs[0].func,
|
||||||
|
})
|
||||||
|
build_error_contents |= new_build_error_content
|
||||||
|
existing_fingerprints.append(fingerprint)
|
||||||
|
|
||||||
|
for build_error_content in build_error_contents:
|
||||||
|
logs = hash_dict[build_error_content.fingerprint]
|
||||||
|
for rec in logs:
|
||||||
|
if rec.build_id not in build_error_content.build_ids:
|
||||||
|
self.env['runbot.build.error.link'].create({
|
||||||
|
'build_id': rec.build_id.id,
|
||||||
|
'error_content_id': build_error_content.id,
|
||||||
|
'log_date': rec.create_date,
|
||||||
|
})
|
||||||
|
|
||||||
|
if build_error_contents:
|
||||||
|
window_action = {
|
||||||
|
"type": "ir.actions.act_window",
|
||||||
|
"res_model": "runbot.build.error",
|
||||||
|
"views": [[False, "tree"]],
|
||||||
|
"domain": [('id', 'in', build_error_contents.ids)]
|
||||||
|
}
|
||||||
|
if len(build_error_contents) == 1:
|
||||||
|
window_action["views"] = [[False, "form"]]
|
||||||
|
window_action["res_id"] = build_error_contents.id
|
||||||
|
return window_action
|
||||||
|
|
||||||
|
def action_link_errors(self):
|
||||||
|
if len(self) < 2:
|
||||||
|
return
|
||||||
|
# sort self so that the first one is the one that has test tags or responsible, or the oldest.
|
||||||
|
self_sorted = self.sorted(lambda error: (not error.test_tags, not error.responsible, error.error_count, error.id))
|
||||||
|
base_error = self_sorted[0]
|
||||||
|
base_error._merge(self_sorted - base_error)
|
||||||
|
|
||||||
|
|
||||||
|
class BuildErrorContent(models.Model):
|
||||||
|
|
||||||
|
_name = 'runbot.build.error.content'
|
||||||
|
_description = "Build error log"
|
||||||
|
|
||||||
|
_inherit = ('mail.thread', 'mail.activity.mixin', 'runbot.build.error.seen.mixin')
|
||||||
|
_rec_name = "id"
|
||||||
|
|
||||||
|
error_id = fields.Many2one('runbot.build.error', 'Linked to', index=True, required=True)
|
||||||
|
content = fields.Text('Error message', required=True)
|
||||||
|
cleaned_content = fields.Text('Cleaned error message')
|
||||||
|
summary = fields.Char('Content summary', compute='_compute_summary', store=False)
|
||||||
|
module_name = fields.Char('Module name') # name in ir_logging
|
||||||
|
file_path = fields.Char('File Path') # path in ir logging
|
||||||
|
function = fields.Char('Function name') # func name in ir logging
|
||||||
|
fingerprint = fields.Char('Error fingerprint', index=True)
|
||||||
|
random = fields.Boolean('underterministic error', tracking=True)
|
||||||
|
build_error_link_ids = fields.One2many('runbot.build.error.link', 'error_content_id')
|
||||||
|
|
||||||
|
build_ids = fields.Many2many('runbot.build', compute='_compute_build_ids')
|
||||||
|
bundle_ids = fields.One2many('runbot.bundle', compute='_compute_bundle_ids')
|
||||||
|
version_ids = fields.One2many('runbot.version', compute='_compute_version_ids', string='Versions', search='_search_version')
|
||||||
|
trigger_ids = fields.Many2many('runbot.trigger', compute='_compute_trigger_ids', string='Triggers', search='_search_trigger_ids')
|
||||||
|
tag_ids = fields.Many2many('runbot.build.error.tag', string='Tags')
|
||||||
|
|
||||||
|
responsible = fields.Many2one(related='error_id.responsible')
|
||||||
|
customer = fields.Many2one(related='error_id.customer')
|
||||||
|
team_id = fields.Many2one(related='error_id.team_id')
|
||||||
|
fixing_commit = fields.Char(related='error_id.fixing_commit')
|
||||||
|
fixing_pr_id = fields.Many2one(related='error_id.fixing_pr_id')
|
||||||
|
fixing_pr_alive = fields.Boolean(related='error_id.fixing_pr_alive')
|
||||||
|
fixing_pr_url = fields.Char(related='error_id.fixing_pr_url')
|
||||||
|
test_tags = fields.Char(related='error_id.test_tags')
|
||||||
|
tags_min_version_id = fields.Many2one(related='error_id.tags_min_version_id')
|
||||||
|
tags_max_version_id = fields.Many2one(related='error_id.tags_max_version_id')
|
||||||
|
|
||||||
|
def _set_error_history(self):
|
||||||
|
for error_content in self:
|
||||||
|
if not error_content.error_id.previous_error_id:
|
||||||
|
previous_error_content = error_content.search([
|
||||||
|
('fingerprint', '=', error_content.fingerprint),
|
||||||
|
('error_id.active', '=', False),
|
||||||
|
('id', '!=', error_content.id or False),
|
||||||
|
])
|
||||||
|
if previous_error_content and previous_error_content != error_content.error_id:
|
||||||
|
error_content.error_id.message_post(body=f"An historical error was found for error {error_content.id}: {previous_error_content.id}")
|
||||||
|
error_content.error_id.previous_error_id = previous_error_content.error_id
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
cleaners = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
||||||
|
for vals in vals_list:
|
||||||
|
if not vals.get('error_id'):
|
||||||
|
# TODO, try to find an existing one that could match, will be done in another pr
|
||||||
|
name = vals.get('content', '').split('\n')[0][:1000]
|
||||||
|
error = self.env['runbot.build.error'].create({
|
||||||
|
'name': name,
|
||||||
|
})
|
||||||
|
vals['error_id'] = error.id
|
||||||
|
content = vals.get('content')
|
||||||
|
cleaned_content = cleaners._r_sub(content)
|
||||||
|
vals.update({
|
||||||
|
'cleaned_content': cleaned_content,
|
||||||
|
'fingerprint': self._digest(cleaned_content)
|
||||||
|
})
|
||||||
|
records = super().create(vals_list)
|
||||||
|
records._set_error_history()
|
||||||
|
records.error_id.action_assign()
|
||||||
|
return records
|
||||||
|
|
||||||
|
def write(self, vals):
|
||||||
|
if 'cleaned_content' in vals:
|
||||||
|
vals.update({'fingerprint': self._digest(vals['cleaned_content'])})
|
||||||
|
initial_errors = self.mapped('error_id')
|
||||||
|
result = super().write(vals)
|
||||||
|
if vals.get('error_id'):
|
||||||
|
for build_error, previous_error in zip(self, initial_errors):
|
||||||
|
if not previous_error.error_content_ids:
|
||||||
|
build_error.error_id._merge(previous_error)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@api.depends('build_error_link_ids')
|
||||||
|
def _compute_build_ids(self):
|
||||||
|
for record in self:
|
||||||
|
record.build_ids = record.build_error_link_ids.mapped('build_id').sorted('id')
|
||||||
|
|
||||||
|
@api.depends('build_ids')
|
||||||
|
def _compute_bundle_ids(self):
|
||||||
|
for build_error in self:
|
||||||
|
top_parent_builds = build_error.build_ids.mapped(lambda rec: rec and rec.top_parent)
|
||||||
|
build_error.bundle_ids = top_parent_builds.mapped('slot_ids').mapped('batch_id.bundle_id')
|
||||||
|
|
||||||
|
@api.depends('build_ids')
|
||||||
|
def _compute_version_ids(self):
|
||||||
|
for build_error in self:
|
||||||
|
build_error.version_ids = build_error.build_ids.version_id
|
||||||
|
|
||||||
|
@api.depends('build_ids')
|
||||||
|
def _compute_trigger_ids(self):
|
||||||
|
for build_error in self:
|
||||||
|
build_error.trigger_ids = build_error.build_ids.trigger_id
|
||||||
|
|
||||||
|
@api.depends('content')
|
||||||
|
def _compute_summary(self):
|
||||||
|
for build_error in self:
|
||||||
|
build_error.summary = build_error.content[:80]
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _digest(self, s):
|
||||||
|
"""
|
||||||
|
return a hash 256 digest of the string s
|
||||||
|
"""
|
||||||
|
return hashlib.sha256(s.encode()).hexdigest()
|
||||||
|
|
||||||
def _search_version(self, operator, value):
|
def _search_version(self, operator, value):
|
||||||
exclude_domain = []
|
exclude_domain = []
|
||||||
if operator == '=':
|
if operator == '=':
|
||||||
@ -303,93 +441,63 @@ class BuildError(models.Model):
|
|||||||
def _search_trigger_ids(self, operator, value):
|
def _search_trigger_ids(self, operator, value):
|
||||||
return [('build_error_link_ids.trigger_id', operator, value)]
|
return [('build_error_link_ids.trigger_id', operator, value)]
|
||||||
|
|
||||||
def _get_form_url(self):
|
|
||||||
self.ensure_one()
|
|
||||||
return url_join(self.get_base_url(), f'/web#id={self.id}&model=runbot.build.error&view_type=form')
|
|
||||||
|
|
||||||
def _get_form_link(self):
|
|
||||||
self.ensure_one()
|
|
||||||
return Markup(f'<a href="%s">%s</a>') % (self._get_form_url(), self.id)
|
|
||||||
|
|
||||||
def _merge(self):
|
def _merge(self):
|
||||||
if len(self) < 2:
|
if len(self) < 2:
|
||||||
return
|
return
|
||||||
_logger.debug('Merging errors %s', self)
|
_logger.debug('Merging errors %s', self)
|
||||||
base_error = self[0]
|
base_error_content = self[0]
|
||||||
base_linked = self[0].parent_id or self[0]
|
base_error = base_error_content.error_id
|
||||||
for error in self[1:]:
|
errors = self.env['runbot.build.error']
|
||||||
assert base_error.fingerprint == error.fingerprint, f'Errors {base_error.id} and {error.id} have a different fingerprint'
|
for error_content in self[1:]:
|
||||||
if error.test_tags and not base_linked.test_tags:
|
assert base_error_content.fingerprint == error_content.fingerprint, f'Errors {base_error_content.id} and {error_content.id} have a different fingerprint'
|
||||||
base_linked.test_tags = error.test_tags
|
for build_error_link in error_content.build_error_link_ids:
|
||||||
if not base_linked.active and error.active:
|
if build_error_link.build_id not in base_error_content.build_error_link_ids.build_id:
|
||||||
base_linked.active = True
|
build_error_link.error_content_id = base_error_content
|
||||||
base_error.message_post(body=Markup('⚠ test-tags inherited from error %s') % error._get_form_link())
|
|
||||||
elif base_linked.test_tags and error.test_tags and base_linked.test_tags != error.test_tags:
|
|
||||||
base_error.message_post(body=Markup('⚠ trying to merge errors with different test-tags from %s tag: "%s"') % (error._get_form_link(), error.test_tags))
|
|
||||||
error.message_post(body=Markup('⚠ trying to merge errors with different test-tags from %s tag: "%s"') % (base_error._get_form_link(), base_error.test_tags))
|
|
||||||
continue
|
|
||||||
|
|
||||||
for build_error_link in error.build_error_link_ids:
|
|
||||||
if build_error_link.build_id not in base_error.build_error_link_ids.build_id:
|
|
||||||
build_error_link.build_error_id = base_error
|
|
||||||
else:
|
else:
|
||||||
# as the relation already exists and was not transferred we can remove the old one
|
# as the relation already exists and was not transferred we can remove the old one
|
||||||
build_error_link.unlink()
|
build_error_link.unlink()
|
||||||
|
if error_content.error_id != base_error_content.error_id:
|
||||||
if error.responsible and not base_linked.responsible:
|
base_error.message_post(body=Markup('Error content coming from %s was merged into this one') % error_content.error_id._get_form_link())
|
||||||
base_error.responsible = error.responsible
|
if not base_error.active and error_content.error_id.active:
|
||||||
elif base_linked.responsible and error.responsible and base_linked.responsible != error.responsible:
|
base_error.active = True
|
||||||
base_linked.message_post(body=Markup('⚠ responsible in merged error %s was "%s" and different from this one') % (error._get_form_link(), error.responsible.name))
|
errors |= error_content.error_id
|
||||||
|
error_content.unlink()
|
||||||
if error.team_id and not base_error.team_id:
|
for error in errors:
|
||||||
base_error.team_id = error.team_id
|
error.message_post(body=Markup('Some error contents from this error where merged into %s') % base_error._get_form_link())
|
||||||
|
if not error.error_content_ids:
|
||||||
base_error.message_post(body=Markup('Error %s was merged into this one') % error._get_form_link())
|
base_error._merge(error)
|
||||||
error.message_post(body=Markup('Error was merged into %s') % base_linked._get_form_link())
|
|
||||||
error.child_ids.parent_id = base_error
|
|
||||||
error.active = False
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Actions
|
# Actions
|
||||||
####################
|
####################
|
||||||
|
|
||||||
def action_link_errors(self):
|
def action_link_errors_contents(self):
|
||||||
""" Link errors with the first one of the recordset
|
""" Link errors with the first one of the recordset
|
||||||
choosing parent in error with responsible, random bug and finally fisrt seen
|
choosing parent in error with responsible, random bug and finally fisrt seen
|
||||||
"""
|
"""
|
||||||
if len(self) < 2:
|
if len(self) < 2:
|
||||||
return
|
return
|
||||||
self = self.with_context(active_test=False)
|
# sort self so that the first one is the one that has test tags or responsible, or the oldest.
|
||||||
build_errors = self.search([('id', 'in', self.ids)], order='responsible asc, random desc, id asc')
|
self_sorted = self.sorted(lambda ec: (not ec.error_id.test_tags, not ec.error_id.responsible, ec.error_id.error_count, ec.id))
|
||||||
build_errors[1:].write({'parent_id': build_errors[0].id})
|
base_error = self_sorted[0].error_id
|
||||||
|
base_error._merge(self_sorted.error_id - base_error)
|
||||||
|
|
||||||
def action_clean_content(self):
|
def action_clean_content(self):
|
||||||
_logger.info('Cleaning %s build errors', len(self))
|
_logger.info('Cleaning %s build errorscontent', len(self))
|
||||||
cleaning_regs = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
cleaning_regs = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
||||||
|
|
||||||
changed_fingerprints = set()
|
changed_fingerprints = set()
|
||||||
for build_error in self:
|
for build_error_content in self:
|
||||||
fingerprint_before = build_error.fingerprint
|
fingerprint_before = build_error_content.fingerprint
|
||||||
build_error.cleaned_content = cleaning_regs._r_sub(build_error.content)
|
build_error_content.cleaned_content = cleaning_regs._r_sub(build_error_content.content)
|
||||||
if fingerprint_before != build_error.fingerprint:
|
if fingerprint_before != build_error_content.fingerprint:
|
||||||
changed_fingerprints.add(build_error.fingerprint)
|
changed_fingerprints.add(build_error_content.fingerprint)
|
||||||
|
|
||||||
# merge identical errors
|
# merge identical errors
|
||||||
errors_by_fingerprint = self.env['runbot.build.error'].search([('fingerprint', 'in', list(changed_fingerprints))])
|
errors_content_by_fingerprint = self.env['runbot.build.error.content'].search([('fingerprint', 'in', list(changed_fingerprints))])
|
||||||
for fingerprint in changed_fingerprints:
|
for fingerprint in changed_fingerprints:
|
||||||
errors_to_merge = errors_by_fingerprint.filtered(lambda r: r.fingerprint == fingerprint)
|
errors_content_to_merge = errors_content_by_fingerprint.filtered(lambda r: r.fingerprint == fingerprint)
|
||||||
errors_to_merge._merge()
|
errors_content_to_merge._merge()
|
||||||
|
|
||||||
def action_assign(self):
|
|
||||||
if not any((not record.responsible and not record.team_id and record.file_path and not record.parent_id) for record in self):
|
|
||||||
return
|
|
||||||
teams = self.env['runbot.team'].search(['|', ('path_glob', '!=', False), ('module_ownership_ids', '!=', False)])
|
|
||||||
repos = self.env['runbot.repo'].search([])
|
|
||||||
for record in self:
|
|
||||||
if not record.responsible and not record.team_id and record.file_path and not record.parent_id:
|
|
||||||
team = teams._get_team(record.file_path, repos)
|
|
||||||
if team:
|
|
||||||
record.team_id = team
|
|
||||||
|
|
||||||
|
|
||||||
class BuildErrorTag(models.Model):
|
class BuildErrorTag(models.Model):
|
||||||
@ -398,7 +506,7 @@ class BuildErrorTag(models.Model):
|
|||||||
_description = "Build error tag"
|
_description = "Build error tag"
|
||||||
|
|
||||||
name = fields.Char('Tag')
|
name = fields.Char('Tag')
|
||||||
error_ids = fields.Many2many('runbot.build.error', string='Errors')
|
error_content_ids = fields.Many2many('runbot.build.error.content', string='Errors')
|
||||||
|
|
||||||
|
|
||||||
class ErrorRegex(models.Model):
|
class ErrorRegex(models.Model):
|
||||||
|
@ -113,7 +113,6 @@ class Commit(models.Model):
|
|||||||
_logger.info('git export: exporting to %s (already exists)', export_path)
|
_logger.info('git export: exporting to %s (already exists)', export_path)
|
||||||
return export_path
|
return export_path
|
||||||
|
|
||||||
|
|
||||||
_logger.info('git export: exporting to %s (new)', export_path)
|
_logger.info('git export: exporting to %s (new)', export_path)
|
||||||
os.makedirs(export_path)
|
os.makedirs(export_path)
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ class IrLogging(models.Model):
|
|||||||
build_id = fields.Many2one('runbot.build', 'Build', index=True, ondelete='cascade')
|
build_id = fields.Many2one('runbot.build', 'Build', index=True, ondelete='cascade')
|
||||||
active_step_id = fields.Many2one('runbot.build.config.step', 'Active step', index=True)
|
active_step_id = fields.Many2one('runbot.build.config.step', 'Active step', index=True)
|
||||||
type = fields.Selection(selection_add=TYPES, string='Type', required=True, index=True, ondelete={t[0]: 'cascade' for t in TYPES})
|
type = fields.Selection(selection_add=TYPES, string='Type', required=True, index=True, ondelete={t[0]: 'cascade' for t in TYPES})
|
||||||
error_id = fields.Many2one('runbot.build.error', compute='_compute_known_error') # remember to never store this field
|
error_content_id = fields.Many2one('runbot.build.error.content', compute='_compute_known_error') # remember to never store this field
|
||||||
dbname = fields.Char(string='Database Name', index=False)
|
dbname = fields.Char(string='Database Name', index=False)
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
@ -57,12 +57,12 @@ class IrLogging(models.Model):
|
|||||||
cleaning_regexes = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
cleaning_regexes = self.env['runbot.error.regex'].search([('re_type', '=', 'cleaning')])
|
||||||
fingerprints = defaultdict(list)
|
fingerprints = defaultdict(list)
|
||||||
for ir_logging in self:
|
for ir_logging in self:
|
||||||
ir_logging.error_id = False
|
ir_logging.error_content_id = False
|
||||||
if ir_logging.level in ('ERROR', 'CRITICAL', 'WARNING') and ir_logging.type == 'server':
|
if ir_logging.level in ('ERROR', 'CRITICAL', 'WARNING') and ir_logging.type == 'server':
|
||||||
fingerprints[self.env['runbot.build.error']._digest(cleaning_regexes._r_sub(ir_logging.message))].append(ir_logging)
|
fingerprints[self.env['runbot.build.error.content']._digest(cleaning_regexes._r_sub(ir_logging.message))].append(ir_logging)
|
||||||
for build_error in self.env['runbot.build.error'].search([('fingerprint', 'in', list(fingerprints.keys()))], order='active asc'):
|
for build_error_content in self.env['runbot.build.error.content'].search([('fingerprint', 'in', list(fingerprints.keys()))]).sorted(lambda ec: not ec.error_id.active):
|
||||||
for ir_logging in fingerprints[build_error.fingerprint]:
|
for ir_logging in fingerprints[build_error_content.fingerprint]:
|
||||||
ir_logging.error_id = build_error.id
|
ir_logging.error_content_id = build_error_content.id
|
||||||
|
|
||||||
def _prepare_create_values(self, vals_list):
|
def _prepare_create_values(self, vals_list):
|
||||||
# keep the given create date
|
# keep the given create date
|
||||||
@ -160,9 +160,8 @@ class RunbotErrorLog(models.Model):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def _parse_logs(self):
|
def _parse_logs(self):
|
||||||
BuildError = self.env['runbot.build.error']
|
|
||||||
ir_logs = self.env['ir.logging'].browse(self.ids)
|
ir_logs = self.env['ir.logging'].browse(self.ids)
|
||||||
return BuildError._parse_logs(ir_logs)
|
return self.env['runbot.build.error']._parse_logs(ir_logs)
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
""" Create an SQL view for ir.logging """
|
""" Create an SQL view for ir.logging """
|
||||||
|
@ -27,7 +27,7 @@ class RunbotTeam(models.Model):
|
|||||||
organisation = fields.Char('organisation', related="project_id.organisation")
|
organisation = fields.Char('organisation', related="project_id.organisation")
|
||||||
user_ids = fields.Many2many('res.users', string='Team Members', domain=[('share', '=', False)])
|
user_ids = fields.Many2many('res.users', string='Team Members', domain=[('share', '=', False)])
|
||||||
dashboard_id = fields.Many2one('runbot.dashboard', string='Dashboard')
|
dashboard_id = fields.Many2one('runbot.dashboard', string='Dashboard')
|
||||||
build_error_ids = fields.One2many('runbot.build.error', 'team_id', string='Team Errors', domain=[('parent_id', '=', False)])
|
assignment_ids = fields.One2many('runbot.build.error', 'team_id', string='Team Errors')
|
||||||
path_glob = fields.Char(
|
path_glob = fields.Char(
|
||||||
'Module Wildcards',
|
'Module Wildcards',
|
||||||
help='Comma separated list of `fnmatch` wildcards used to assign errors automaticaly\n'
|
help='Comma separated list of `fnmatch` wildcards used to assign errors automaticaly\n'
|
||||||
|
@ -21,6 +21,8 @@ access_runbot_config_step_upgrade_db_manager,runbot_config_step_upgrade_db_manag
|
|||||||
|
|
||||||
access_runbot_build_error_user,runbot_build_error_user,runbot.model_runbot_build_error,group_user,1,0,0,0
|
access_runbot_build_error_user,runbot_build_error_user,runbot.model_runbot_build_error,group_user,1,0,0,0
|
||||||
access_runbot_build_error_admin,runbot_build_error_admin,runbot.model_runbot_build_error,runbot.group_runbot_admin,1,1,1,1
|
access_runbot_build_error_admin,runbot_build_error_admin,runbot.model_runbot_build_error,runbot.group_runbot_admin,1,1,1,1
|
||||||
|
access_runbot_build_error_content_user,runbot_build_error_content_user,runbot.model_runbot_build_error_content,group_user,1,0,0,0
|
||||||
|
access_runbot_build_error_content_admin,runbot_build_error_content_admin,runbot.model_runbot_build_error_content,runbot.group_runbot_admin,1,1,1,1
|
||||||
access_runbot_build_error_manager,runbot_build_error_manager,runbot.model_runbot_build_error,runbot.group_runbot_error_manager,1,1,1,1
|
access_runbot_build_error_manager,runbot_build_error_manager,runbot.model_runbot_build_error,runbot.group_runbot_error_manager,1,1,1,1
|
||||||
access_runbot_build_error_link_user,runbot_runbot_build_error_link_user,runbot.model_runbot_build_error_link,group_user,1,0,0,0
|
access_runbot_build_error_link_user,runbot_runbot_build_error_link_user,runbot.model_runbot_build_error_link,group_user,1,0,0,0
|
||||||
access_runbot_build_error_link_admin,runbot_runbot_build_error_link_admin,runbot.model_runbot_build_error_link,runbot.group_runbot_admin,1,1,1,1
|
access_runbot_build_error_link_admin,runbot_runbot_build_error_link_admin,runbot.model_runbot_build_error_link,runbot.group_runbot_admin,1,1,1,1
|
||||||
|
|
@ -7,10 +7,12 @@ import { Many2OneField } from "@web/views/fields/many2one/many2one_field";
|
|||||||
import { _lt } from "@web/core/l10n/translation";
|
import { _lt } from "@web/core/l10n/translation";
|
||||||
import { registry } from "@web/core/registry";
|
import { registry } from "@web/core/registry";
|
||||||
import { useDynamicPlaceholder } from "@web/views/fields/dynamic_placeholder_hook";
|
import { useDynamicPlaceholder } from "@web/views/fields/dynamic_placeholder_hook";
|
||||||
|
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||||
import { useInputField } from "@web/views/fields/input_field_hook";
|
import { useInputField } from "@web/views/fields/input_field_hook";
|
||||||
|
|
||||||
import { useRef, xml, Component } from "@odoo/owl";
|
import { useRef, xml, Component } from "@odoo/owl";
|
||||||
import { useAutoresize } from "@web/core/utils/autoresize";
|
import { useAutoresize } from "@web/core/utils/autoresize";
|
||||||
|
import { getFormattedValue } from "@web/views/utils";
|
||||||
|
|
||||||
|
|
||||||
function stringify(obj) {
|
function stringify(obj) {
|
||||||
@ -62,16 +64,32 @@ registry.category("fields").add("runbotjsonb", {
|
|||||||
|
|
||||||
export class FrontendUrl extends Component {
|
export class FrontendUrl extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
<div class="o_field_many2one_selection">
|
<div><a t-att-href="route" target="_blank"><t t-esc="displayValue"/></a></div>
|
||||||
<div class="o_field_widget"><Many2OneField t-props="props"/></div>
|
`;
|
||||||
<div><a t-att-href="route" target="_blank"><span class="fa fa-play ms-2"/></a></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
static components = { Many2OneField };
|
static components = { Many2OneField };
|
||||||
|
|
||||||
|
static props = {
|
||||||
|
...Many2OneField.props,
|
||||||
|
linkField: { type: String, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
get baseProps() {
|
||||||
|
console.log(omit(this.props, 'linkField'))
|
||||||
|
return omit(this.props, 'linkField', 'context')
|
||||||
|
}
|
||||||
|
|
||||||
|
get displayValue() {
|
||||||
|
return this.props.record.data[this.props.name] ? getFormattedValue(this.props.record, this.props.name, {}) : ''
|
||||||
|
}
|
||||||
|
|
||||||
get route() {
|
get route() {
|
||||||
const model = this.props.relation || this.props.record.fields[this.props.name].relation;
|
return this._route(this.props.linkField || this.props.name)
|
||||||
const id = this.props.record.data[this.props.name][0];
|
}
|
||||||
|
|
||||||
|
_route(fieldName) {
|
||||||
|
const model = this.props.record.fields[fieldName].relation || "runbot.unknown";
|
||||||
|
const id = this.props.record.data[fieldName][0];
|
||||||
if (model.startsWith('runbot.') ) {
|
if (model.startsWith('runbot.') ) {
|
||||||
return '/runbot/' + model.split('.')[1] + '/' + id;
|
return '/runbot/' + model.split('.')[1] + '/' + id;
|
||||||
} else {
|
} else {
|
||||||
@ -83,6 +101,11 @@ export class FrontendUrl extends Component {
|
|||||||
registry.category("fields").add("frontend_url", {
|
registry.category("fields").add("frontend_url", {
|
||||||
supportedTypes: ["many2one"],
|
supportedTypes: ["many2one"],
|
||||||
component: FrontendUrl,
|
component: FrontendUrl,
|
||||||
|
extractProps({ attrs, options }, dynamicInfo) {
|
||||||
|
return {
|
||||||
|
linkField: options.link_field,
|
||||||
|
};
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -333,7 +333,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td t-attf-class="bg-{{message_class.strip() or logclass}}-subtle">
|
<td t-attf-class="bg-{{message_class.strip() or logclass}}-subtle">
|
||||||
<t t-if="l.level in ('CRITICAL', 'ERROR', 'WARNING') and not l.with_context(active_test=False).error_id">
|
<t t-if="l.level in ('CRITICAL', 'ERROR', 'WARNING') and not l.with_context(active_test=False).error_content_id">
|
||||||
<small>
|
<small>
|
||||||
<a groups="runbot.group_runbot_5admin" t-attf-href="/runbot/parse_log/{{l.id}}" class="sm" title="Parse this log line to follow this error.">
|
<a groups="runbot.group_runbot_5admin" t-attf-href="/runbot/parse_log/{{l.id}}" class="sm" title="Parse this log line to follow this error.">
|
||||||
<i t-attf-class="fa fa-magic"/>
|
<i t-attf-class="fa fa-magic"/>
|
||||||
@ -342,21 +342,18 @@
|
|||||||
</t>
|
</t>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<t t-if="l.with_context(active_test=False).error_id">
|
<t t-if="l.error_content_id">
|
||||||
<t t-set="icon" t-value="'list'"/>
|
<t t-set="error_content" t-value="l.error_content_id"/>
|
||||||
<t t-set="error" t-value="l.error_id"/>
|
<t t-set="error" t-value="error_content.error_id"/>
|
||||||
<t t-set="size" t-value=""/>
|
|
||||||
<t t-if="l.error_id.parent_id">
|
|
||||||
<t t-set="icon" t-value="'link'"/>
|
|
||||||
<t t-set="error" t-value="l.error_id.parent_id"/>
|
|
||||||
<t t-set="size" t-value="'small'"/>
|
|
||||||
</t>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td/><td/><td/>
|
<td/><td/><td/>
|
||||||
<td t-attf-class="bg-{{'info' if error.active else 'success'}}-subtle {{size}}" colspan="2">
|
<td t-attf-class="bg-{{'info' if error.active else 'success'}}-subtle" colspan="2">
|
||||||
This error is already <a href="#" t-attf-title="{{'Was detected by runbot in nightly builds.' if error.active else 'Either the error is not properly fixed or the branch does not contain the fix.'}}"><t t-esc="'known' if error.active else 'fixed'"/></a>.
|
This error is already <em t-attf-title="{{'Was detected by runbot in nightly builds.' if error.active else 'Either the error is not properly fixed or the branch does not contain the fix.'}}"><t t-esc="'known' if error.active else 'fixed'"/></em>.
|
||||||
<a groups="runbot.group_user" t-attf-href="/web#id={{l.error_id.id}}&view_type=form&model=runbot.build.error&menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" title="View in Backend" target="new">
|
<!--a groups="runbot.group_user" t-attf-href="/web#id={{error_content.id}}&view_type=form&model=runbot.build.error.content&menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" title="View in Backend" target="new">
|
||||||
<i t-attf-class="fa fa-{{icon}}"/>
|
<i t-attf-class="fa fa-search"/>
|
||||||
|
</a-->
|
||||||
|
<a groups="runbot.group_user" t-attf-href="/web#id={{error.id}}&view_type=form&model=runbot.build.error&menu_id={{env['ir.model.data']._xmlid_to_res_id('runbot.runbot_menu_root')}}" title="View in Backend" target="new">
|
||||||
|
<i t-attf-class="fa fa-list"/>
|
||||||
</a>
|
</a>
|
||||||
<span groups="runbot.group_runbot_admin" t-if="error.responsible or error.responsible.id == uid">(<i t-esc="error.responsible.name"/>)</span>
|
<span groups="runbot.group_runbot_admin" t-if="error.responsible or error.responsible.id == uid">(<i t-esc="error.responsible.name"/>)</span>
|
||||||
</td>
|
</td>
|
||||||
|
@ -7,8 +7,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">Last seen date</div>
|
<div class="col">Last seen date</div>
|
||||||
<div class="col col-md-3">Module</div>
|
<div class="col col-md-8">Summary</div>
|
||||||
<div class="col col-md-5">Summary</div>
|
|
||||||
<div class="col">Triggers</div>
|
<div class="col">Triggers</div>
|
||||||
<div class="col">Assigned to</div>
|
<div class="col">Assigned to</div>
|
||||||
<div class="col">&nbsp;</div>
|
<div class="col">&nbsp;</div>
|
||||||
@ -20,10 +19,9 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col"><t t-esc="build_error.last_seen_date" t-options='{"widget": "datetime"}'/></div>
|
<div class="col"><t t-esc="build_error.last_seen_date" t-options='{"widget": "datetime"}'/></div>
|
||||||
<div class="col col-md-3"><t t-esc="build_error.module_name"/></div>
|
<div class="col col-md-8">
|
||||||
<div class="col col-md-5">
|
|
||||||
<button class="btn accordion-button collapsed" type="button" data-bs-toggle="collapse" t-attf-data-bs-target="#collapse{{build_error.id}}" aria-expanded="true" aria-controls="collapseOne">
|
<button class="btn accordion-button collapsed" type="button" data-bs-toggle="collapse" t-attf-data-bs-target="#collapse{{build_error.id}}" aria-expanded="true" aria-controls="collapseOne">
|
||||||
<code><t t-esc="build_error.summary"/></code>
|
<code><t t-esc="build_error.name"/></code>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
@ -125,14 +123,14 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
<h3 t-if="build_error_ids">Team assigned Errors</h3>
|
<h3 t-if="assignment_ids">Team assigned Errors</h3>
|
||||||
<t t-call="portal.portal_searchbar">
|
<t t-call="portal.portal_searchbar">
|
||||||
<t t-set="classes" t-valuef="o_runbot_team_searchbar border-0"/>
|
<t t-set="classes" t-valuef="o_runbot_team_searchbar border-0"/>
|
||||||
<t t-set="title">&nbsp;</t>
|
<t t-set="title">&nbsp;</t>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<t t-call="runbot.build_error_cards">
|
<t t-call="runbot.build_error_cards">
|
||||||
<t t-set="build_errors" t-value="build_error_ids"/>
|
<t t-set="build_errors" t-value="assignment_ids"/>
|
||||||
<t t-set="accordion_id">team_errors</t>
|
<t t-set="accordion_id">team_errors</t>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,8 +55,9 @@ class TestBuildError(RunbotCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestBuildError, self).setUp()
|
super(TestBuildError, self).setUp()
|
||||||
self.BuildError = self.env['runbot.build.error']
|
self.BuildError = self.env['runbot.build.error']
|
||||||
|
self.BuildErrorContent = self.env['runbot.build.error.content']
|
||||||
self.BuildErrorLink = self.env['runbot.build.error.link']
|
self.BuildErrorLink = self.env['runbot.build.error.link']
|
||||||
self.BuildErrorTeam = self.env['runbot.team']
|
self.RunbotTeam = self.env['runbot.team']
|
||||||
self.ErrorRegex = self.env['runbot.error.regex']
|
self.ErrorRegex = self.env['runbot.error.regex']
|
||||||
self.IrLog = self.env['ir.logging']
|
self.IrLog = self.env['ir.logging']
|
||||||
|
|
||||||
@ -67,37 +68,132 @@ class TestBuildError(RunbotCase):
|
|||||||
're_type': 'cleaning',
|
're_type': 'cleaning',
|
||||||
})
|
})
|
||||||
|
|
||||||
error_x = self.BuildError.create({
|
error_content = self.BuildErrorContent.create({
|
||||||
'content': 'foo bar 242',
|
'content': 'foo bar 242',
|
||||||
})
|
})
|
||||||
|
|
||||||
expected = 'foo bar %'
|
expected = 'foo bar %'
|
||||||
expected_hash = hashlib.sha256(expected.encode()).hexdigest()
|
expected_hash = hashlib.sha256(expected.encode()).hexdigest()
|
||||||
self.assertEqual(error_x.cleaned_content, expected)
|
self.assertEqual(error_content.cleaned_content, expected)
|
||||||
self.assertEqual(error_x.fingerprint, expected_hash)
|
self.assertEqual(error_content.fingerprint, expected_hash)
|
||||||
|
|
||||||
# Let's ensure that the fingerprint changes if we clean with an additional regex
|
# Let's ensure that the fingerprint changes if we clean with an additional regex
|
||||||
self.ErrorRegex.create({
|
self.ErrorRegex.create({
|
||||||
'regex': 'bar',
|
'regex': 'bar',
|
||||||
're_type': 'cleaning',
|
're_type': 'cleaning',
|
||||||
})
|
})
|
||||||
error_x.action_clean_content()
|
error_content.action_clean_content()
|
||||||
expected = 'foo % %'
|
expected = 'foo % %'
|
||||||
expected_hash = hashlib.sha256(expected.encode()).hexdigest()
|
expected_hash = hashlib.sha256(expected.encode()).hexdigest()
|
||||||
self.assertEqual(error_x.cleaned_content, expected)
|
self.assertEqual(error_content.cleaned_content, expected)
|
||||||
self.assertEqual(error_x.fingerprint, expected_hash)
|
self.assertEqual(error_content.fingerprint, expected_hash)
|
||||||
|
|
||||||
def test_merge(self):
|
def test_fields(self):
|
||||||
|
version_1 = self.Version.create({'name': '1.0'})
|
||||||
|
version_2 = self.Version.create({'name': '2.0'})
|
||||||
|
bundle_1 = self.Bundle.create({'name': 'v1', 'project_id': self.project.id})
|
||||||
|
bundle_2 = self.Bundle.create({'name': 'v2', 'project_id': self.project.id})
|
||||||
|
batch_1 = self.Batch.create({'bundle_id': bundle_1.id})
|
||||||
|
batch_2 = self.Batch.create({'bundle_id': bundle_2.id})
|
||||||
|
|
||||||
|
params_1 = self.BuildParameters.create({
|
||||||
|
'version_id': version_1.id,
|
||||||
|
'project_id': self.project.id,
|
||||||
|
'config_id': self.default_config.id,
|
||||||
|
'create_batch_id': batch_1.id,
|
||||||
|
})
|
||||||
|
params_2 = self.BuildParameters.create({
|
||||||
|
'version_id': version_2.id,
|
||||||
|
'project_id': self.project.id,
|
||||||
|
'config_id': self.default_config.id,
|
||||||
|
'create_batch_id': batch_2.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
build_1 = self.Build.create({
|
||||||
|
'local_result': 'ko',
|
||||||
|
'local_state': 'done',
|
||||||
|
'params_id': params_1.id,
|
||||||
|
})
|
||||||
|
build_2 = self.Build.create({
|
||||||
|
'local_result': 'ko',
|
||||||
|
'local_state': 'done',
|
||||||
|
'params_id': params_2.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
self.env['runbot.batch.slot'].create({
|
||||||
|
'build_id': build_1.id,
|
||||||
|
'batch_id': batch_1.id,
|
||||||
|
'params_id': build_1.params_id.id,
|
||||||
|
'link_type': 'created',
|
||||||
|
})
|
||||||
|
self.env['runbot.batch.slot'].create({
|
||||||
|
'build_id': build_2.id,
|
||||||
|
'batch_id': batch_2.id,
|
||||||
|
'params_id': build_2.params_id.id,
|
||||||
|
'link_type': 'created',
|
||||||
|
})
|
||||||
|
|
||||||
|
error = self.BuildError.create({})
|
||||||
|
error_content_1 = self.BuildErrorContent.create({'content': 'foo bar v1', 'error_id': error.id})
|
||||||
|
error_content_2 = self.BuildErrorContent.create({'content': 'foo bar v2', 'error_id': error.id})
|
||||||
|
error_content_2b = self.BuildErrorContent.create({'content': 'bar v2', 'error_id': error.id})
|
||||||
|
l_1 = self.BuildErrorLink.create({'build_id': build_1.id, 'error_content_id': error_content_1.id})
|
||||||
|
l_2 = self.BuildErrorLink.create({'build_id': build_2.id, 'error_content_id': error_content_2.id})
|
||||||
|
l_3 = self.BuildErrorLink.create({'build_id': build_2.id, 'error_content_id': error_content_2b.id})
|
||||||
|
|
||||||
|
self.assertEqual(error_content_1.build_ids, build_1)
|
||||||
|
self.assertEqual(error_content_2.build_ids, build_2)
|
||||||
|
self.assertEqual(error_content_2b.build_ids, build_2)
|
||||||
|
self.assertEqual(error.build_ids, build_1 | build_2)
|
||||||
|
|
||||||
|
self.assertEqual(error_content_1.bundle_ids, bundle_1)
|
||||||
|
self.assertEqual(error_content_2.bundle_ids, bundle_2)
|
||||||
|
self.assertEqual(error_content_2b.bundle_ids, bundle_2)
|
||||||
|
self.assertEqual(error.bundle_ids, bundle_1 | bundle_2)
|
||||||
|
|
||||||
|
self.assertEqual(error_content_1.version_ids, version_1)
|
||||||
|
self.assertEqual(error_content_2.version_ids, version_2)
|
||||||
|
self.assertEqual(error_content_2b.version_ids, version_2)
|
||||||
|
self.assertEqual(error.version_ids, version_1 | version_2)
|
||||||
|
|
||||||
|
self.assertEqual(error_content_1.build_error_link_ids, l_1)
|
||||||
|
self.assertEqual(error_content_2.build_error_link_ids, l_2)
|
||||||
|
self.assertEqual(error_content_2b.build_error_link_ids, l_3)
|
||||||
|
self.assertEqual(error.build_error_link_ids, l_1 | l_2 | l_3)
|
||||||
|
self.assertEqual(error.unique_build_error_link_ids, l_1 | l_2)
|
||||||
|
|
||||||
|
def test_merge_test_tags(self):
|
||||||
|
error_a = self.BuildError.create({
|
||||||
|
'content': 'foo',
|
||||||
|
})
|
||||||
|
error_b = self.BuildError.create({
|
||||||
|
'content': 'bar',
|
||||||
|
'test_tags': 'blah',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(self.BuildError._disabling_tags(), ['-blah'])
|
||||||
|
|
||||||
|
error_a._merge(error_b)
|
||||||
|
|
||||||
|
self.assertEqual(self.BuildError._disabling_tags(), ['-blah'])
|
||||||
|
self.assertEqual(error_a.test_tags, 'blah')
|
||||||
|
self.assertEqual(error_b.test_tags, False)
|
||||||
|
self.assertEqual(error_b.active, False)
|
||||||
|
|
||||||
|
def test_merge_contents(self):
|
||||||
build_a = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
build_a = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
error_a = self.BuildError.create({'content': 'foo bar'})
|
error_content_a = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
self.BuildErrorLink.create({'build_id': build_a.id, 'build_error_id': error_a.id})
|
self.BuildErrorLink.create({'build_id': build_a.id, 'error_content_id': error_content_a.id})
|
||||||
|
error_a = error_content_a.error_id
|
||||||
|
|
||||||
build_b = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
build_b = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
error_b = self.BuildError.create({'content': 'foo bar'})
|
error_content_b = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
self.BuildErrorLink.create({'build_id': build_b.id, 'build_error_id': error_b.id})
|
self.BuildErrorLink.create({'build_id': build_b.id, 'error_content_id': error_content_b.id})
|
||||||
|
error_b = error_content_b.error_id
|
||||||
(error_a | error_b)._merge()
|
self.assertNotEqual(error_a, error_b)
|
||||||
self.assertEqual(len(self.BuildError.search([('fingerprint', '=', error_a.fingerprint)])), 1)
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a | error_content_b)
|
||||||
|
(error_content_a | error_content_b)._merge()
|
||||||
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a)
|
||||||
self.assertTrue(error_a.active, 'The first merged error should stay active')
|
self.assertTrue(error_a.active, 'The first merged error should stay active')
|
||||||
self.assertFalse(error_b.active, 'The second merged error should have stay deactivated')
|
self.assertFalse(error_b.active, 'The second merged error should have stay deactivated')
|
||||||
self.assertIn(build_a, error_a.build_error_link_ids.build_id)
|
self.assertIn(build_a, error_a.build_error_link_ids.build_id)
|
||||||
@ -107,50 +203,82 @@ class TestBuildError(RunbotCase):
|
|||||||
self.assertFalse(error_b.build_error_link_ids)
|
self.assertFalse(error_b.build_error_link_ids)
|
||||||
self.assertFalse(error_b.build_ids)
|
self.assertFalse(error_b.build_ids)
|
||||||
|
|
||||||
error_c = self.BuildError.create({'content': 'foo foo'})
|
error_content_c = self.BuildErrorContent.create({'content': 'foo foo'})
|
||||||
|
|
||||||
# let's ensure we cannot merge errors with different fingerprints
|
# let's ensure we cannot merge errors with different fingerprints
|
||||||
with self.assertRaises(AssertionError):
|
with self.assertRaises(AssertionError):
|
||||||
(error_a | error_c)._merge()
|
(error_content_a | error_content_c)._merge()
|
||||||
|
|
||||||
# merge two build errors while the build <--> build_error relation already exists
|
# merge two build errors while the build <--> build_error relation already exists
|
||||||
error_d = self.BuildError.create({'content': 'foo bar'})
|
error_content_d = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
self.BuildErrorLink.create({'build_id': build_a.id, 'build_error_id': error_d.id})
|
self.BuildErrorLink.create({'build_id': build_a.id, 'error_content_id': error_content_d.id})
|
||||||
(error_a | error_d)._merge()
|
(error_content_a | error_content_d)._merge()
|
||||||
self.assertIn(build_a, error_a.build_error_link_ids.build_id)
|
self.assertIn(build_a, error_content_a.build_error_link_ids.build_id)
|
||||||
self.assertIn(build_a, error_a.build_ids)
|
self.assertIn(build_a, error_content_a.build_ids)
|
||||||
self.assertFalse(error_d.build_error_link_ids)
|
self.assertFalse(error_content_d.build_error_link_ids)
|
||||||
self.assertFalse(error_d.build_ids)
|
self.assertFalse(error_content_d.build_ids)
|
||||||
|
|
||||||
def test_merge_linked(self):
|
|
||||||
top_error = self.BuildError.create({'content': 'foo foo', 'active': False})
|
|
||||||
|
|
||||||
|
def test_merge_simple(self):
|
||||||
build_a = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
build_a = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
error_a = self.BuildError.create({'content': 'foo bar', 'parent_id': top_error.id })
|
error_content_a = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
self.BuildErrorLink.create({'build_id': build_a.id, 'build_error_id': error_a.id})
|
error_a = error_content_a.error_id
|
||||||
|
error_a.active = False
|
||||||
|
self.BuildErrorLink.create({'build_id': build_a.id, 'error_content_id': error_content_a.id})
|
||||||
build_b = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
build_b = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
error_b = self.BuildError.create({'content': 'foo bar', 'test_tags': 'footag'})
|
error_content_b = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
self.BuildErrorLink.create({'build_id': build_b.id, 'build_error_id': error_b.id})
|
error_b = error_content_b.error_id
|
||||||
|
error_b.test_tags = 'footag'
|
||||||
|
self.BuildErrorLink.create({'build_id': build_b.id, 'error_content_id': error_content_b.id})
|
||||||
|
|
||||||
linked_error = self.BuildError.create({'content': 'foo foo bar', 'parent_id': error_b.id})
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a | error_content_b)
|
||||||
|
(error_content_a | error_content_b)._merge()
|
||||||
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a)
|
||||||
|
self.assertFalse(error_b.error_content_ids)
|
||||||
|
|
||||||
(error_a | error_b)._merge()
|
self.assertTrue(error_a.active, 'The merged error without test tags should have been deactivated')
|
||||||
self.assertEqual(len(self.BuildError.search([('fingerprint', '=', error_a.fingerprint)])), 1)
|
self.assertEqual(error_a.test_tags, 'footag', 'Tags should have been transfered from b to a')
|
||||||
self.assertTrue(error_a.active, 'The first merged error should stay active')
|
self.assertFalse(error_b.active, 'The merged error with test tags should remain active')
|
||||||
self.assertFalse(error_b.active, 'The second merged error should have stay deactivated')
|
self.assertIn(build_a, error_content_a.build_ids)
|
||||||
self.assertIn(build_a, error_a.build_ids)
|
self.assertIn(build_b, error_content_a.build_ids)
|
||||||
self.assertIn(build_b, error_a.build_ids)
|
self.assertFalse(error_content_b.build_ids)
|
||||||
self.assertFalse(error_b.build_ids)
|
self.assertEqual(error_a.active, True)
|
||||||
self.assertEqual(top_error.test_tags, 'footag')
|
|
||||||
self.assertEqual(top_error.active, True)
|
|
||||||
self.assertEqual(linked_error.parent_id, error_a, 'Linked errors to a merged one should be now linked to the new one')
|
|
||||||
|
|
||||||
tagged_error = self.BuildError.create({'content': 'foo foo', 'test_tags': 'bartag'})
|
tagged_error_content = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
(top_error | tagged_error)._merge()
|
tagged_error = tagged_error_content.error_id
|
||||||
self.assertTrue(top_error.active)
|
tagged_error.test_tags = 'bartag'
|
||||||
|
(error_content_a | tagged_error_content)._merge()
|
||||||
|
self.assertEqual(error_a.test_tags, 'footag')
|
||||||
|
self.assertEqual(tagged_error.test_tags, 'bartag')
|
||||||
|
self.assertTrue(error_a.active)
|
||||||
self.assertTrue(tagged_error.active, 'A differently tagged error cannot be deactivated by the merge')
|
self.assertTrue(tagged_error.active, 'A differently tagged error cannot be deactivated by the merge')
|
||||||
|
|
||||||
|
def test_merge_linked(self):
|
||||||
|
build_a = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
|
error_content_a = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
|
error_a = error_content_a.error_id
|
||||||
|
error_a.active = False
|
||||||
|
self.BuildErrorLink.create({'build_id': build_a.id, 'error_content_id': error_content_a.id})
|
||||||
|
build_b = self.create_test_build({'local_result': 'ko', 'local_state': 'done'})
|
||||||
|
error_content_b = self.BuildErrorContent.create({'content': 'foo bar'})
|
||||||
|
error_b = error_content_b.error_id
|
||||||
|
error_b.test_tags = 'footag'
|
||||||
|
self.BuildErrorLink.create({'build_id': build_b.id, 'error_content_id': error_content_b.id})
|
||||||
|
|
||||||
|
linked_error = self.BuildErrorContent.create({'content': 'foo foo bar', 'error_id': error_b.id})
|
||||||
|
|
||||||
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a | error_content_b)
|
||||||
|
(error_content_a | error_content_b)._merge()
|
||||||
|
self.assertEqual(self.BuildErrorContent.search([('fingerprint', '=', error_content_a.fingerprint)]), error_content_a)
|
||||||
|
self.assertEqual(error_b.error_content_ids, linked_error)
|
||||||
|
self.assertTrue(error_a.active, 'Main error should have been reactivated')
|
||||||
|
self.assertEqual(error_a.test_tags, False, 'Tags should remain on b')
|
||||||
|
self.assertEqual(error_b.test_tags, 'footag', 'Tags should remain on b')
|
||||||
|
self.assertTrue(error_b.active, 'The merged error with test tags should remain active')
|
||||||
|
self.assertIn(build_a, error_content_a.build_ids)
|
||||||
|
self.assertIn(build_b, error_content_a.build_ids)
|
||||||
|
self.assertFalse(error_content_b.build_ids)
|
||||||
|
self.assertEqual(error_a.active, True)
|
||||||
|
self.assertEqual(linked_error.error_id, error_b)
|
||||||
|
|
||||||
def test_build_scan(self):
|
def test_build_scan(self):
|
||||||
ko_build = self.create_test_build({'local_result': 'ok', 'local_state': 'testing'})
|
ko_build = self.create_test_build({'local_result': 'ok', 'local_state': 'testing'})
|
||||||
@ -168,7 +296,7 @@ class TestBuildError(RunbotCase):
|
|||||||
'replacement': "''",
|
'replacement': "''",
|
||||||
})
|
})
|
||||||
|
|
||||||
error_team = self.BuildErrorTeam.create({
|
error_team = self.RunbotTeam.create({
|
||||||
'name': 'test-error-team',
|
'name': 'test-error-team',
|
||||||
'path_glob': '*/test_ui.py'
|
'path_glob': '*/test_ui.py'
|
||||||
})
|
})
|
||||||
@ -193,22 +321,24 @@ class TestBuildError(RunbotCase):
|
|||||||
ok_build._parse_logs()
|
ok_build._parse_logs()
|
||||||
build_error = ko_build.build_error_ids
|
build_error = ko_build.build_error_ids
|
||||||
self.assertTrue(build_error)
|
self.assertTrue(build_error)
|
||||||
self.assertTrue(build_error.fingerprint.startswith('af0e88f3'))
|
error_content = build_error.error_content_ids
|
||||||
self.assertTrue(build_error.cleaned_content.startswith('%'), 'The cleaner should have replace "FAIL: " with a "%" sign by default')
|
self.assertTrue(error_content.fingerprint.startswith('af0e88f3'))
|
||||||
self.assertFalse('^' in build_error.cleaned_content, 'The cleaner should have removed the "^" chars')
|
self.assertTrue(error_content.cleaned_content.startswith('%'), 'The cleaner should have replace "FAIL: " with a "%" sign by default')
|
||||||
error_link = self.env['runbot.build.error.link'].search([('build_id', '=', ko_build.id), ('build_error_id', '=', build_error.id)])
|
self.assertFalse('^' in error_content.cleaned_content, 'The cleaner should have removed the "^" chars')
|
||||||
|
error_link = self.env['runbot.build.error.link'].search([('build_id', '=', ko_build.id), ('error_content_id', '=', error_content.id)])
|
||||||
self.assertTrue(error_link, 'An error link should exists')
|
self.assertTrue(error_link, 'An error link should exists')
|
||||||
self.assertIn(ko_build, build_error.build_error_link_ids.mapped('build_id'), 'Ko build should be in build_error_link_ids')
|
self.assertIn(ko_build, error_content.build_ids, 'Ko build should be in build_error_link_ids')
|
||||||
self.assertEqual(error_link.log_date, fields.Datetime.from_string('2023-08-29 00:46:21'))
|
self.assertEqual(error_link.log_date, fields.Datetime.from_string('2023-08-29 00:46:21'))
|
||||||
self.assertIn(ko_build, build_error.build_ids, 'The parsed build should be added to the runbot.build.error')
|
self.assertIn(ko_build, error_content.build_ids, 'The parsed build should be added to the runbot.build.error')
|
||||||
self.assertFalse(self.BuildErrorLink.search([('build_id', '=', ok_build.id)]), 'A successful build should not be associated to a runbot.build.error')
|
self.assertFalse(self.BuildErrorLink.search([('build_id', '=', ok_build.id)]), 'A successful build should not be associated to a runbot.build.error')
|
||||||
self.assertEqual(error_team, build_error.team_id)
|
self.assertEqual(error_content.file_path, '/data/build/server/addons/web_studio/tests/test_ui.py')
|
||||||
|
self.assertEqual(build_error.team_id, error_team)
|
||||||
|
|
||||||
# Test that build with same error is added to the errors
|
# Test that build with same error is added to the errors
|
||||||
ko_build_same_error = self.create_test_build({'local_result': 'ko'})
|
ko_build_same_error = self.create_test_build({'local_result': 'ko'})
|
||||||
self.create_log({'create_date': fields.Datetime.from_string('2023-08-29 01:46:21'), 'message': RTE_ERROR, 'build_id': ko_build_same_error.id})
|
self.create_log({'create_date': fields.Datetime.from_string('2023-08-29 01:46:21'), 'message': RTE_ERROR, 'build_id': ko_build_same_error.id})
|
||||||
ko_build_same_error._parse_logs()
|
ko_build_same_error._parse_logs()
|
||||||
self.assertIn(ko_build_same_error, build_error.build_ids, 'The parsed build should be added to the existing runbot.build.error')
|
self.assertIn(ko_build_same_error, error_content.build_ids, 'The parsed build should be added to the existing runbot.build.error')
|
||||||
|
|
||||||
# Test that line numbers does not interfere with error recognition
|
# Test that line numbers does not interfere with error recognition
|
||||||
ko_build_diff_number = self.create_test_build({'local_result': 'ko'})
|
ko_build_diff_number = self.create_test_build({'local_result': 'ko'})
|
||||||
@ -224,9 +354,9 @@ class TestBuildError(RunbotCase):
|
|||||||
self.create_log({'create_date': fields.Datetime.from_string('2023-08-29 01:46:21'), 'message': RTE_ERROR, 'build_id': ko_build_new.id})
|
self.create_log({'create_date': fields.Datetime.from_string('2023-08-29 01:46:21'), 'message': RTE_ERROR, 'build_id': ko_build_new.id})
|
||||||
ko_build_new._parse_logs()
|
ko_build_new._parse_logs()
|
||||||
self.assertNotIn(ko_build_new, build_error.build_ids, 'The parsed build should not be added to a fixed runbot.build.error')
|
self.assertNotIn(ko_build_new, build_error.build_ids, 'The parsed build should not be added to a fixed runbot.build.error')
|
||||||
new_build_error = self.BuildErrorLink.search([('build_id', '=', ko_build_new.id)]).mapped('build_error_id')
|
new_build_error = self.BuildErrorLink.search([('build_id', '=', ko_build_new.id)]).error_content_id.error_id
|
||||||
self.assertIn(ko_build_new, new_build_error.build_ids, 'The parsed build with a re-apearing error should generate a new runbot.build.error')
|
self.assertIn(ko_build_new, new_build_error.build_ids, 'The parsed build with a re-apearing error should generate a new runbot.build.error')
|
||||||
self.assertIn(build_error, new_build_error.error_history_ids, 'The old error should appear in history')
|
self.assertEqual(build_error, new_build_error.previous_error_id, 'The old error should appear in history')
|
||||||
|
|
||||||
def test_seen_date(self):
|
def test_seen_date(self):
|
||||||
# create all the records before the tests to evaluate compute dependencies
|
# create all the records before the tests to evaluate compute dependencies
|
||||||
@ -261,9 +391,9 @@ class TestBuildError(RunbotCase):
|
|||||||
# a new build error is linked to the current one
|
# a new build error is linked to the current one
|
||||||
build_c._parse_logs()
|
build_c._parse_logs()
|
||||||
build_error_c = build_c.build_error_ids
|
build_error_c = build_c.build_error_ids
|
||||||
self.assertNotIn(build_c, build_error_a.children_build_ids)
|
self.assertNotIn(build_c, build_error_a.build_ids)
|
||||||
build_error_c.parent_id = build_error_a
|
build_error_a._merge(build_error_c)
|
||||||
self.assertIn(build_c, build_error_a.children_build_ids)
|
self.assertIn(build_c, build_error_a.build_ids)
|
||||||
self.assertEqual(build_error_a.last_seen_date, child_seen_date)
|
self.assertEqual(build_error_a.last_seen_date, child_seen_date)
|
||||||
self.assertEqual(build_error_a.last_seen_build_id, build_c)
|
self.assertEqual(build_error_a.last_seen_build_id, build_c)
|
||||||
|
|
||||||
@ -276,40 +406,28 @@ class TestBuildError(RunbotCase):
|
|||||||
build_a = self.create_test_build({'local_result': 'ko'})
|
build_a = self.create_test_build({'local_result': 'ko'})
|
||||||
build_b = self.create_test_build({'local_result': 'ko'})
|
build_b = self.create_test_build({'local_result': 'ko'})
|
||||||
|
|
||||||
error_a = self.env['runbot.build.error'].create({
|
error_content_a = self.env['runbot.build.error.content'].create({
|
||||||
'content': 'foo',
|
'content': 'foo',
|
||||||
'active': False # Even a fixed error coul be linked
|
|
||||||
})
|
})
|
||||||
|
|
||||||
self.BuildErrorLink.create({'build_id': build_a.id, 'build_error_id': error_a.id})
|
self.BuildErrorLink.create({'build_id': build_a.id, 'error_content_id': error_content_a.id})
|
||||||
|
error_content_b = self.env['runbot.build.error.content'].create({
|
||||||
error_b = self.env['runbot.build.error'].create({
|
|
||||||
'content': 'bar',
|
'content': 'bar',
|
||||||
'random': True
|
'random': True
|
||||||
})
|
})
|
||||||
|
self.BuildErrorLink.create({'build_id': build_b.id, 'error_content_id': error_content_b.id})
|
||||||
self.BuildErrorLink.create({'build_id': build_b.id, 'build_error_id': error_b.id})
|
|
||||||
|
|
||||||
# test that the random bug is parent when linking errors
|
# test that the random bug is parent when linking errors
|
||||||
all_errors = error_a | error_b
|
self.assertNotEqual(error_content_a.error_id, error_content_b.error_id)
|
||||||
all_errors.action_link_errors()
|
all_errors = error_content_a | error_content_b
|
||||||
self.assertEqual(error_b.child_ids, error_a, 'Random error should be the parent')
|
all_errors.action_link_errors_contents()
|
||||||
|
self.assertEqual(error_content_a.error_id, error_content_b.error_id, 'Error should be linked')
|
||||||
# Test that changing bug resolution is propagated to children
|
|
||||||
error_b.active = True
|
|
||||||
self.assertTrue(error_a.active)
|
|
||||||
error_b.active = False
|
|
||||||
self.assertFalse(error_a.active)
|
|
||||||
|
|
||||||
# Test build_ids
|
# Test build_ids
|
||||||
self.assertIn(build_b, error_b.build_ids)
|
self.assertEqual(build_a, error_content_a.build_ids)
|
||||||
self.assertNotIn(build_a, error_b.build_ids)
|
self.assertEqual(build_b, error_content_b.build_ids)
|
||||||
|
error = error_content_a.error_id
|
||||||
# Test that children builds contains all builds
|
self.assertEqual(build_a | build_b, error.build_ids)
|
||||||
self.assertIn(build_b, error_b.children_build_ids)
|
|
||||||
self.assertIn(build_a, error_b.children_build_ids)
|
|
||||||
self.assertEqual(error_a.build_count, 1)
|
|
||||||
self.assertEqual(error_b.build_count, 2)
|
|
||||||
|
|
||||||
def test_build_error_test_tags_no_version(self):
|
def test_build_error_test_tags_no_version(self):
|
||||||
build_a = self.create_test_build({'local_result': 'ko'})
|
build_a = self.create_test_build({'local_result': 'ko'})
|
||||||
@ -337,12 +455,6 @@ class TestBuildError(RunbotCase):
|
|||||||
# test that test tags on fixed errors are not taken into account
|
# test that test tags on fixed errors are not taken into account
|
||||||
self.assertNotIn('-blah', self.BuildError._disabling_tags())
|
self.assertNotIn('-blah', self.BuildError._disabling_tags())
|
||||||
|
|
||||||
error_a.test_tags = False
|
|
||||||
error_b.active = True
|
|
||||||
error_b.parent_id = error_a.id
|
|
||||||
self.assertEqual(error_b.test_tags, False)
|
|
||||||
self.assertEqual(self.BuildError._disabling_tags(), ['-blah'])
|
|
||||||
|
|
||||||
def test_build_error_test_tags_min_max_version(self):
|
def test_build_error_test_tags_min_max_version(self):
|
||||||
version_17 = self.Version.create({'name': '17.0'})
|
version_17 = self.Version.create({'name': '17.0'})
|
||||||
version_saas_171 = self.Version.create({'name': 'saas-17.1'})
|
version_saas_171 = self.Version.create({'name': 'saas-17.1'})
|
||||||
@ -389,7 +501,7 @@ class TestBuildError(RunbotCase):
|
|||||||
self.assertEqual(sorted(['-every', '-where', '-tag_17_up_to_master']), sorted(self.BuildError._disabling_tags(build_master)))
|
self.assertEqual(sorted(['-every', '-where', '-tag_17_up_to_master']), sorted(self.BuildError._disabling_tags(build_master)))
|
||||||
|
|
||||||
def test_build_error_team_wildcards(self):
|
def test_build_error_team_wildcards(self):
|
||||||
website_team = self.BuildErrorTeam.create({
|
website_team = self.RunbotTeam.create({
|
||||||
'name': 'website_test',
|
'name': 'website_test',
|
||||||
'path_glob': '*website*,-*website_sale*'
|
'path_glob': '*website*,-*website_sale*'
|
||||||
})
|
})
|
||||||
@ -402,11 +514,11 @@ class TestBuildError(RunbotCase):
|
|||||||
self.assertEqual(website_team, teams._get_team('/data/build/odoo/addons/website/tests/test_ui'))
|
self.assertEqual(website_team, teams._get_team('/data/build/odoo/addons/website/tests/test_ui'))
|
||||||
|
|
||||||
def test_build_error_team_ownership(self):
|
def test_build_error_team_ownership(self):
|
||||||
website_team = self.BuildErrorTeam.create({
|
website_team = self.RunbotTeam.create({
|
||||||
'name': 'website_test',
|
'name': 'website_test',
|
||||||
'path_glob': ''
|
'path_glob': ''
|
||||||
})
|
})
|
||||||
sale_team = self.BuildErrorTeam.create({
|
sale_team = self.RunbotTeam.create({
|
||||||
'name': 'sale_test',
|
'name': 'sale_test',
|
||||||
'path_glob': ''
|
'path_glob': ''
|
||||||
})
|
})
|
||||||
|
@ -1,35 +1,115 @@
|
|||||||
<odoo>
|
<odoo>
|
||||||
<data>
|
<data>
|
||||||
<record id="build_error_form" model="ir.ui.view">
|
<record id="runbot_build_error_form" model="ir.ui.view">
|
||||||
<field name="name">runbot.build.error.form</field>
|
<field name="name">runbot.build.error.form</field>
|
||||||
<field name="model">runbot.build.error</field>
|
<field name="model">runbot.build.error</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
<sheet>
|
<sheet>
|
||||||
<widget name="web_ribbon" title="Test-tags" bg_color="bg-danger" invisible="not test_tags"/>
|
<widget name="web_ribbon" title="Test-tags" bg_color="bg-danger" invisible="not test_tags"/>
|
||||||
<widget name="web_ribbon" title="Linked to another error" bg_color="bg-warning" invisible="not parent_id"/>
|
<button name="action_view_errors" string="See all linked errors" type="object" class="oe_highlight"/>
|
||||||
<header>
|
<group string="Base info">
|
||||||
</header>
|
<field name="name"/>
|
||||||
|
<field name="error_content_ids" readonly="1">
|
||||||
|
<tree>
|
||||||
|
<field name="content" readonly="1"/>
|
||||||
|
<!--field name="module_name" readonly="1"/-->
|
||||||
|
<!--field name="function" readonly="1"/-->
|
||||||
|
<!--field name="file_path" readonly="1"/-->
|
||||||
|
<field name="version_ids" widget="many2many_tags" optional="hide"/>
|
||||||
|
<field name="trigger_ids" widget="many2many_tags" optional="hide"/>
|
||||||
|
<field name="tag_ids" widget="many2many_tags" readonly="1" optional="hide"/>
|
||||||
|
<field name="random" optional="hide"/>
|
||||||
|
<field name="first_seen_date" widget="frontend_url" options="{'link_field': 'first_seen_build_id'}"/>
|
||||||
|
<field name="last_seen_date" widget="frontend_url" options="{'link_field': 'last_seen_build_id'}"/>
|
||||||
|
<field name="first_seen_build_id" column_invisible="True"/>
|
||||||
|
<field name="last_seen_build_id" column_invisible="True"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
<group name="fixer_info" string="Fixing" col="2">
|
||||||
|
<group>
|
||||||
|
<field name="responsible"/>
|
||||||
|
<field name="customer"/>
|
||||||
|
<field name="team_id"/>
|
||||||
|
<field name="fixing_pr_id"/>
|
||||||
|
<field name="fixing_pr_url" widget="url"/>
|
||||||
|
<field name="active"/>
|
||||||
|
<field name="test_tags" decoration-danger="True" readonly="1" groups="!runbot.group_runbot_admin"/>
|
||||||
|
<field name="test_tags" decoration-danger="True" groups="runbot.group_runbot_admin"/>
|
||||||
|
<field name="tags_min_version_id" invisible="not test_tags"/>
|
||||||
|
<field name="tags_max_version_id" invisible="not test_tags"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="version_ids" widget="many2many_tags"/>
|
||||||
|
<field name="trigger_ids" widget="many2many_tags"/>
|
||||||
|
<field name="tag_ids" widget="many2many_tags"/>
|
||||||
|
<field name="random"/>
|
||||||
|
<field name="first_seen_date" widget="frontend_url" options="{'link_field': 'first_seen_build_id'}"/>
|
||||||
|
<field name="last_seen_date" widget="frontend_url" options="{'link_field': 'last_seen_build_id'}"/>
|
||||||
|
<field name="first_seen_build_id" invisible="True"/>
|
||||||
|
<field name="last_seen_build_id" invisible="True"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="previous_error_id" readonly="1" invisible="not previous_error_id" text-decoration-danger="True"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Builds">
|
||||||
|
<field name="unique_build_error_link_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
||||||
|
<tree default_order="log_date desc,id desc">
|
||||||
|
<field name="log_date"/>
|
||||||
|
<field name="host" groups="base.group_no_one" optional="hide"/>
|
||||||
|
<field name="dest" optional="hide"/>
|
||||||
|
<field name="version_id"/>
|
||||||
|
<field name="trigger_id"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
<page string="All links">
|
||||||
|
<field name="build_error_link_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
||||||
|
<tree default_order="log_date desc,id desc">
|
||||||
|
<field name="log_date"/>
|
||||||
|
<field name="host" groups="base.group_no_one" optional="hide"/>
|
||||||
|
<field name="dest" optional="hide"/>
|
||||||
|
<field name="version_id"/>
|
||||||
|
<field name="trigger_id"/>
|
||||||
|
<field name="description"/>
|
||||||
|
<field name="error_content_id" widget="many2one" string="Linked Error log"/>
|
||||||
|
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<field name="message_ids"/>
|
||||||
|
<field name="activity_ids"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="build_error_form" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.build.error.content.form</field>
|
||||||
|
<field name="model">runbot.build.error.content</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="error_id"/>
|
||||||
|
</group>
|
||||||
<group name="build_error_group" string="Base info" col="2">
|
<group name="build_error_group" string="Base info" col="2">
|
||||||
<field name="content" readonly="1"/>
|
<field name="content" readonly="1"/>
|
||||||
<field name="module_name" readonly="1"/>
|
<field name="module_name" readonly="1"/>
|
||||||
<field name="function" readonly="1"/>
|
<field name="function" readonly="1"/>
|
||||||
<field name="file_path" readonly="1"/>
|
<field name="file_path" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="fixer_info" string="Fixing" col="2">
|
<group name="infos" string="" col="2">
|
||||||
<group>
|
<group>
|
||||||
<field name="responsible" readonly="parent_id and not responsible"/>
|
|
||||||
<field name="team_id" readonly="parent_id and not team_id"/>
|
|
||||||
<field name="fixing_pr_id"/>
|
|
||||||
<field name="fixing_pr_url" widget="url"/>
|
|
||||||
<field name="active"/>
|
|
||||||
<field name="test_tags" decoration-danger="True" readonly="1" groups="!runbot.group_runbot_admin"/>
|
|
||||||
<field name="test_tags" decoration-danger="True" groups="runbot.group_runbot_admin" readonly="parent_id and not test_tags"/>
|
|
||||||
<field name="tags_min_version_id" invisible="not test_tags"/>
|
|
||||||
<field name="tags_max_version_id" invisible="not test_tags"/>
|
|
||||||
</group>
|
|
||||||
<group>
|
|
||||||
<field name="customer"/>
|
|
||||||
<field name="version_ids" widget="many2many_tags"/>
|
<field name="version_ids" widget="many2many_tags"/>
|
||||||
<field name="trigger_ids" widget="many2many_tags"/>
|
<field name="trigger_ids" widget="many2many_tags"/>
|
||||||
<field name="tag_ids" widget="many2many_tags" readonly="1"/>
|
<field name="tag_ids" widget="many2many_tags" readonly="1"/>
|
||||||
@ -42,14 +122,13 @@
|
|||||||
<field name="first_seen_build_id" widget="frontend_url"/>
|
<field name="first_seen_build_id" widget="frontend_url"/>
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="parent_id" decoration-warning="parent_id != False"/>
|
|
||||||
<field name="last_seen_date"/>
|
<field name="last_seen_date"/>
|
||||||
<field name="last_seen_build_id" widget="frontend_url"/>
|
<field name="last_seen_build_id" widget="frontend_url"/>
|
||||||
</group>
|
</group>
|
||||||
</group>
|
</group>
|
||||||
<notebook>
|
<notebook>
|
||||||
<page string="Builds">
|
<page string="Builds">
|
||||||
<field name="children_build_error_link_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
<field name="build_error_link_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
||||||
<tree>
|
<tree>
|
||||||
<field name="log_date"/>
|
<field name="log_date"/>
|
||||||
<field name="host" groups="base.group_no_one"/>
|
<field name="host" groups="base.group_no_one"/>
|
||||||
@ -57,42 +136,15 @@
|
|||||||
<field name="version_id"/>
|
<field name="version_id"/>
|
||||||
<field name="trigger_id"/>
|
<field name="trigger_id"/>
|
||||||
<field name="description"/>
|
<field name="description"/>
|
||||||
<field name="build_error_id" widget="many2one" string="Linked Error"/>
|
<field name="error_content_id" widget="many2one" string="Linked Error"/>
|
||||||
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
<field name="build_url" widget="url" readonly="1" text="View build"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page string="Linked Errors" invisible="child_ids == []">
|
|
||||||
<field name="child_ids" widget="many2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
|
||||||
<tree>
|
|
||||||
<field name="create_date"/>
|
|
||||||
<field name="module_name"/>
|
|
||||||
<field name="summary"/>
|
|
||||||
<field name="build_count"/>
|
|
||||||
<button type="object" name="get_formview_action" icon="fa-arrow-right" title="View linked error"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
<page string="Error history" invisible="error_history_ids == []">
|
|
||||||
<field name="error_history_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
|
|
||||||
<tree>
|
|
||||||
<field name="create_date"/>
|
|
||||||
<field name="module_name"/>
|
|
||||||
<field name="summary"/>
|
|
||||||
<field name="random"/>
|
|
||||||
<field name="build_count"/>
|
|
||||||
<field name="responsible"/>
|
|
||||||
<field name="fixing_commit"/>
|
|
||||||
<field name="id"/>
|
|
||||||
<button type="object" name="get_formview_action" icon="fa-arrow-right" title="View linked error"/>
|
|
||||||
</tree>
|
|
||||||
</field>
|
|
||||||
</page>
|
|
||||||
<page string="Debug" groups="base.group_no_one">
|
<page string="Debug" groups="base.group_no_one">
|
||||||
<group name="build_error_group">
|
<group name="build_error_group">
|
||||||
<field name="fingerprint" readonly="1"/>
|
<field name="fingerprint" readonly="1"/>
|
||||||
<field name="cleaned_content" readonly="1"/>
|
<field name="cleaned_content" readonly="1"/>
|
||||||
<field name="fixing_commit" widget="url"/>
|
|
||||||
<field name="bundle_ids" widget="many2many_tags"/>
|
<field name="bundle_ids" widget="many2many_tags"/>
|
||||||
</group>
|
</group>
|
||||||
</page>
|
</page>
|
||||||
@ -119,7 +171,6 @@
|
|||||||
</group>
|
</group>
|
||||||
<group name="Fix">
|
<group name="Fix">
|
||||||
<field name="fixing_pr_id"/>
|
<field name="fixing_pr_id"/>
|
||||||
<field name="fixing_commit"/>
|
|
||||||
</group>
|
</group>
|
||||||
<group>
|
<group>
|
||||||
<field name="chatter_comment"/>
|
<field name="chatter_comment"/>
|
||||||
@ -149,6 +200,38 @@
|
|||||||
<record id="build_error_view_tree" model="ir.ui.view">
|
<record id="build_error_view_tree" model="ir.ui.view">
|
||||||
<field name="name">runbot.build.error.tree</field>
|
<field name="name">runbot.build.error.tree</field>
|
||||||
<field name="model">runbot.build.error</field>
|
<field name="model">runbot.build.error</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Errors"
|
||||||
|
decoration-danger="test_tags and (fixing_pr_alive or not fixing_pr_id)"
|
||||||
|
decoration-success="fixing_pr_id and not test_tags and not fixing_pr_alive"
|
||||||
|
decoration-warning="test_tags and fixing_pr_id and not fixing_pr_alive"
|
||||||
|
multi_edit="1"
|
||||||
|
>
|
||||||
|
<header>
|
||||||
|
<button name="%(runbot.runbot_open_bulk_wizard)d" string="Bulk Update" type="action" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager"/>
|
||||||
|
</header>
|
||||||
|
<field name="name" optional="show" readonly="1"/>
|
||||||
|
<field name="description" optional="hide" readonly="1"/>
|
||||||
|
<field name="random" string="Random"/>
|
||||||
|
<field name="first_seen_date" string="First Seen" optional="hide" readonly="1"/>
|
||||||
|
<field name="last_seen_date" string="Last Seen" readonly="1" options="{'link_field': 'last_seen_build_id'}"/>
|
||||||
|
<field name="last_seen_build_id" column_invisible="True"/>
|
||||||
|
<field name="error_count" readonly="1"/>
|
||||||
|
<field name="build_count" readonly="1"/>
|
||||||
|
<field name="team_id"/>
|
||||||
|
<field name="test_tags" optional="hide"/>
|
||||||
|
<field name="tags_min_version_id" string="Tags Min" optional="hide"/>
|
||||||
|
<field name="tags_max_version_id" string="Tags Max" optional="hide"/>
|
||||||
|
<field name="fixing_pr_id" optional="hide"/>
|
||||||
|
<field name="fixing_pr_alive" optional="hide"/>
|
||||||
|
<field name="fixing_pr_url" widget="url" text="view PR" readonly="1" invisible="not fixing_pr_url"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="build_error_content_view_tree" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.build.error.content.tree</field>
|
||||||
|
<field name="model">runbot.build.error.content</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<tree string="Errors"
|
<tree string="Errors"
|
||||||
decoration-danger="test_tags and (fixing_pr_alive or not fixing_pr_id)"
|
decoration-danger="test_tags and (fixing_pr_alive or not fixing_pr_id)"
|
||||||
@ -166,13 +249,12 @@
|
|||||||
<field name="first_seen_date" string="First Seen" optional="hide" readonly="1"/>
|
<field name="first_seen_date" string="First Seen" optional="hide" readonly="1"/>
|
||||||
<field name="last_seen_date" string="Last Seen" readonly="1"/>
|
<field name="last_seen_date" string="Last Seen" readonly="1"/>
|
||||||
<field name="build_count" readonly="1"/>
|
<field name="build_count" readonly="1"/>
|
||||||
<field name="responsible"/>
|
|
||||||
<field name="team_id"/>
|
<field name="team_id"/>
|
||||||
<field name="test_tags"/>
|
<field name="test_tags" optional="hide"/>
|
||||||
<field name="tags_min_version_id" string="Tags Min" optional="show"/>
|
<field name="tags_min_version_id" string="Tags Min" optional="hide"/>
|
||||||
<field name="tags_max_version_id" string="Tags Max" optional="show"/>
|
<field name="tags_max_version_id" string="Tags Max" optional="hide"/>
|
||||||
<field name="fixing_pr_id"/>
|
<field name="fixing_pr_id" optional="hide"/>
|
||||||
<field name="fixing_pr_alive" invisible="1"/>
|
<field name="fixing_pr_alive" optional="hide"/>
|
||||||
<field name="fixing_pr_url" widget="url" text="view PR" readonly="1" invisible="not fixing_pr_url"/>
|
<field name="fixing_pr_url" widget="url" text="view PR" readonly="1" invisible="not fixing_pr_url"/>
|
||||||
<field name="fingerprint" optional="hide"/>
|
<field name="fingerprint" optional="hide"/>
|
||||||
</tree>
|
</tree>
|
||||||
@ -180,23 +262,19 @@
|
|||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="build_error_search_view" model="ir.ui.view">
|
<record id="build_error_search_view" model="ir.ui.view">
|
||||||
<field name="name">runbot.build.error.log.filter</field>
|
<field name="name">runbot.build.error.filter</field>
|
||||||
<field name="model">runbot.build.error</field>
|
<field name="model">runbot.build.error</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<search string="Search errors">
|
<search string="Search errors">
|
||||||
<field name="content"/>
|
<field name="content"/>
|
||||||
<field name="module_name"/>
|
<field name="description"/>
|
||||||
<field name="function"/>
|
|
||||||
<field name="version_ids"/>
|
<field name="version_ids"/>
|
||||||
<field name="responsible"/>
|
<field name="responsible"/>
|
||||||
<field name="team_id"/>
|
<field name="team_id"/>
|
||||||
<field name="fixing_commit"/>
|
|
||||||
<filter string="Assigned to me" name="my_errors" domain="[('responsible', '=', uid)]"/>
|
<filter string="Assigned to me" name="my_errors" domain="[('responsible', '=', uid)]"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="Customer is me" name="my_errors_customer" domain="[('customer', '=', uid)]"/>
|
<filter string="Customer is me" name="my_errors_customer" domain="[('customer', '=', uid)]"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
<filter string="No Parent" name="no_parent_error" domain="[('parent_id', '=', False)]"/>
|
|
||||||
<separator/>
|
|
||||||
<filter string="Undeterministic" name="random_error" domain="[('random', '=', True)]"/>
|
<filter string="Undeterministic" name="random_error" domain="[('random', '=', True)]"/>
|
||||||
<filter string="Deterministic" name="random_error" domain="[('random', '=', False)]"/>
|
<filter string="Deterministic" name="random_error" domain="[('random', '=', False)]"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
@ -217,11 +295,42 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="build_error_content_search_view" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.build.error.content.log.filter</field>
|
||||||
|
<field name="model">runbot.build.error.content</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search string="Search errors">
|
||||||
|
<field name="content"/>
|
||||||
|
<field name="module_name"/>
|
||||||
|
<field name="function"/>
|
||||||
|
<field name="version_ids"/>
|
||||||
|
<filter name="group_error" string="By error" context="{'group_by':'error_id'}"/>
|
||||||
|
<filter string="Undeterministic" name="random_error" domain="[('random', '=', True)]"/>
|
||||||
|
<filter string="Deterministic" name="random_error" domain="[('random', '=', False)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Having a PR" name="pr_set_errors" domain="[('fixing_pr_id', '!=', False)]"/>
|
||||||
|
<filter string="Fixing PR is closed" name="pr_closed_errors" domain="[('fixing_pr_id', '!=', False), ('fixing_pr_id.alive', '=', False)]"/>
|
||||||
|
<filter string="Fixing PR is open" name="pr_open_errors" domain="[('fixing_pr_id', '!=', False), ('fixing_pr_id.alive', '=', True)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Test Tags" name="test_tagged_errors" domain="[('test_tags', '!=', False)]"/>
|
||||||
|
<separator/>
|
||||||
|
<filter string="Not seen in one month" name="not_seen_one_month" domain="[('last_seen_date','<', (context_today() - datetime.timedelta(days=30)).strftime('%Y-%m-%d'))]"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="open_view_build_error_tree" model="ir.actions.act_window">
|
<record id="open_view_build_error_tree" model="ir.actions.act_window">
|
||||||
<field name="name">Build errors</field>
|
<field name="name">Errors</field>
|
||||||
<field name="res_model">runbot.build.error</field>
|
<field name="res_model">runbot.build.error</field>
|
||||||
<field name="view_mode">tree,form</field>
|
<field name="view_mode">tree,form</field>
|
||||||
<field name="context">{'search_default_no_parent_error': True, 'search_default_random_error': True}</field>
|
<field name="context"></field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_view_build_error_content_tree" model="ir.actions.act_window">
|
||||||
|
<field name="name">Build errors contents</field>
|
||||||
|
<field name="res_model">runbot.build.error.content</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
<field name="context">{}</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="build_error_regex_form" model="ir.ui.view">
|
<record id="build_error_regex_form" model="ir.ui.view">
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</page>
|
</page>
|
||||||
<page string="Errors" invisible="build_error_ids == []">
|
<page string="Errors" invisible="bool(assignment_ids)">
|
||||||
<field name="build_error_ids" nolabel="1" widget="many2many" options="{'not_delete': True, 'no_create': True}"/>
|
<field name="assignment_ids" nolabel="1" widget="many2many" options="{'not_delete': True, 'no_create': True}"/>
|
||||||
</page>
|
</page>
|
||||||
<page string="Modules">
|
<page string="Modules">
|
||||||
<field name="module_ownership_ids">
|
<field name="module_ownership_ids">
|
||||||
@ -66,7 +66,7 @@
|
|||||||
<field name="path_glob"/>
|
<field name="path_glob"/>
|
||||||
<field name="github_team"/>
|
<field name="github_team"/>
|
||||||
<field name="module_ownership_ids"/>
|
<field name="module_ownership_ids"/>
|
||||||
<field name="build_error_ids"/>
|
<field name="assignment_ids"/>
|
||||||
</tree>
|
</tree>
|
||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
@ -35,8 +35,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<menuitem name="Manage errors" id="runbot_menu_manage_errors" parent="runbot_menu_root" sequence="900"/>
|
<menuitem name="Manage errors" id="runbot_menu_manage_errors" parent="runbot_menu_root" sequence="900"/>
|
||||||
<menuitem name="Build errors" id="runbot_menu_build_error_tree" parent="runbot_menu_manage_errors" sequence="10" action="open_view_build_error_tree"/>
|
<menuitem name="Errors" id="runbot_menu_build_error_tree" parent="runbot_menu_manage_errors" sequence="5" action="open_view_build_error_tree"/>
|
||||||
<menuitem name="Error Logs" id="runbot_menu_error_logs" parent="runbot_menu_manage_errors" sequence="20" action="open_view_error_log_tree"/>
|
<menuitem name="Errors contents" id="runbot_menu_build_error_content_tree" parent="runbot_menu_manage_errors" sequence="10" action="open_view_build_error_content_tree"/>
|
||||||
|
<menuitem name="Error Logs" id="runbot_menu_error_contents" parent="runbot_menu_manage_errors" sequence="20" action="open_view_error_log_tree"/>
|
||||||
|
|
||||||
<menuitem name="Teams" id="runbot_menu_teams" parent="runbot_menu_root" sequence="1000"/>
|
<menuitem name="Teams" id="runbot_menu_teams" parent="runbot_menu_root" sequence="1000"/>
|
||||||
<menuitem name="Teams" id="runbot_menu_team_tree" parent="runbot_menu_teams" sequence="30" action="open_view_runbot_team"/>
|
<menuitem name="Teams" id="runbot_menu_team_tree" parent="runbot_menu_teams" sequence="30" action="open_view_runbot_team"/>
|
||||||
|
@ -29,6 +29,7 @@ class Runbot(models.AbstractModel):
|
|||||||
assert expected_bundle == existing_bundle
|
assert expected_bundle == existing_bundle
|
||||||
|
|
||||||
if bundles.branch_ids:
|
if bundles.branch_ids:
|
||||||
|
_logger.warning('Skipping populate, bundles already have branches')
|
||||||
# only populate data if no branch are found
|
# only populate data if no branch are found
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -114,7 +115,7 @@ class Runbot(models.AbstractModel):
|
|||||||
_logger.info(command)
|
_logger.info(command)
|
||||||
|
|
||||||
mock_git.side_effect = git
|
mock_git.side_effect = git
|
||||||
with mute_logger('odoo.addons.runbot.models.batch'):
|
batch._prepare()
|
||||||
batch._process()
|
batch._process()
|
||||||
if i != nb_batch - 1:
|
if i != nb_batch - 1:
|
||||||
for slot in batch.slot_ids:
|
for slot in batch.slot_ids:
|
||||||
@ -131,13 +132,12 @@ class Runbot(models.AbstractModel):
|
|||||||
child.description = "Description for security"
|
child.description = "Description for security"
|
||||||
build._log('******', 'Step x finished')
|
build._log('******', 'Step x finished')
|
||||||
build._log('******', 'Starting step Y', level='SEPARATOR')
|
build._log('******', 'Starting step Y', level='SEPARATOR')
|
||||||
build._log('******','Some log', level='ERROR')
|
if not bundle.sticky:
|
||||||
build._log('******','Some log\n with multiple lines', level='ERROR')
|
build._log('******', 'Some log', level='ERROR', log_type='server')
|
||||||
|
build._log('******', 'Some log\n with multiple lines', level='ERROR', log_type='server')
|
||||||
build._log('******', '**Some** *markdown* [log](%s)', 'http://example.com', log_type='markdown')
|
build._log('******', '**Some** *markdown* [log](%s)', 'http://example.com', log_type='markdown')
|
||||||
build._log('******', 'Step x finished', level='SEPARATOR')
|
build._log('******', 'Step x finished', level='SEPARATOR')
|
||||||
|
|
||||||
build.local_state = 'done'
|
build.local_state = 'done'
|
||||||
build.local_result = 'ok' if bundle.sticky else 'ko'
|
build.local_result = 'ok' if bundle.sticky else 'ko'
|
||||||
|
|
||||||
|
|
||||||
batch._process()
|
batch._process()
|
||||||
|
Loading…
Reference in New Issue
Block a user