mirror of
https://github.com/odoo/runbot.git
synced 2025-03-15 23:45:44 +07:00
[IMP] runbot: permit to qualify build errors
Identifying build errors by fingerprint is not enough. Most of the times we want to link build errors based on some criteria. With this commit, a build error can be qualified by using regular expressions that will extract informations. Those informations will be stored in a jsondict field. That way, we can find simmilar build errors when they share some qualifiers. To extract information, the regular expression must use named group patterns. e.g: `^Tour (?P<tour_name>\w+) failed at step (?P<tour_step>.+)` The above regular expression should extract the tour name and tour step from a build error and store them like: `{ "tour_name": "article_portal_tour", "tour_step": "clik on 'Help'" }` The field can be queried to find similar errors. Also, a button is added on the Build Error Form to search for errors that share the same qualifiers.
This commit is contained in:
parent
509c152156
commit
4bdd2e20b8
@ -9,6 +9,9 @@ from markupsafe import Markup
|
|||||||
from werkzeug.urls import url_join
|
from werkzeug.urls import url_join
|
||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
from odoo.exceptions import ValidationError, UserError
|
from odoo.exceptions import ValidationError, UserError
|
||||||
|
from odoo.tools import SQL
|
||||||
|
|
||||||
|
from ..fields import JsonDictField
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -349,6 +352,7 @@ class BuildErrorContent(models.Model):
|
|||||||
version_ids = fields.One2many('runbot.version', compute='_compute_version_ids', string='Versions', search='_search_version')
|
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')
|
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')
|
tag_ids = fields.Many2many('runbot.build.error.tag', string='Tags')
|
||||||
|
qualifiers = JsonDictField('Qualifiers', index=True)
|
||||||
|
|
||||||
responsible = fields.Many2one(related='error_id.responsible')
|
responsible = fields.Many2one(related='error_id.responsible')
|
||||||
customer = fields.Many2one(related='error_id.customer')
|
customer = fields.Many2one(related='error_id.customer')
|
||||||
@ -491,6 +495,17 @@ class BuildErrorContent(models.Model):
|
|||||||
domain = [('id', 'in', self.ids)] if self else []
|
domain = [('id', 'in', self.ids)] if self else []
|
||||||
return [r['id_arr'] for r in self.env['runbot.build.error.content'].read_group(domain, ['id_count:count(id)', 'id_arr:array_agg(id)', 'fingerprint'], ['fingerprint']) if r['id_count'] >1]
|
return [r['id_arr'] for r in self.env['runbot.build.error.content'].read_group(domain, ['id_count:count(id)', 'id_arr:array_agg(id)', 'fingerprint'], ['fingerprint']) if r['id_count'] >1]
|
||||||
|
|
||||||
|
def _qualify(self):
|
||||||
|
qualify_regexes = self.env['runbot.error.qualify.regex'].search([])
|
||||||
|
for record in self:
|
||||||
|
all_qualifiers = {}
|
||||||
|
for qualify_regex in qualify_regexes:
|
||||||
|
res = qualify_regex._qualify(record.content) # TODO, MAYBE choose the source field
|
||||||
|
if res:
|
||||||
|
# res.update({'qualifier_id': qualify_regex.id}) Probably not a good idea
|
||||||
|
all_qualifiers.update(res)
|
||||||
|
record.qualifiers = all_qualifiers
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# Actions
|
# Actions
|
||||||
####################
|
####################
|
||||||
@ -546,6 +561,9 @@ class BuildErrorContent(models.Model):
|
|||||||
'view_mode': 'tree,form'
|
'view_mode': 'tree,form'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def action_qualify(self):
|
||||||
|
self._qualify()
|
||||||
|
|
||||||
|
|
||||||
class BuildErrorTag(models.Model):
|
class BuildErrorTag(models.Model):
|
||||||
|
|
||||||
@ -618,3 +636,76 @@ class ErrorBulkWizard(models.TransientModel):
|
|||||||
if self.chatter_comment:
|
if self.chatter_comment:
|
||||||
for build_error in error_ids:
|
for build_error in error_ids:
|
||||||
build_error.message_post(body=Markup('%s') % self.chatter_comment, subject="Bullk Wizard Comment")
|
build_error.message_post(body=Markup('%s') % self.chatter_comment, subject="Bullk Wizard Comment")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorQualifyRegex(models.Model):
|
||||||
|
|
||||||
|
_name = "runbot.error.qualify.regex"
|
||||||
|
_description = "Build error qualifying regex"
|
||||||
|
_inherit = "mail.thread"
|
||||||
|
_rec_name = 'id'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
sequence = fields.Integer('Sequence', default=100)
|
||||||
|
active = fields.Boolean('Active', default=True, tracking=True)
|
||||||
|
regex = fields.Char('Regular expression', required=True)
|
||||||
|
source_field = fields.Selection(
|
||||||
|
[
|
||||||
|
("content", "Content"),
|
||||||
|
("module", "Module Name"),
|
||||||
|
("function", "Function Name"),
|
||||||
|
("file_path", "File Path"),
|
||||||
|
],
|
||||||
|
default="content",
|
||||||
|
string="Source Field",
|
||||||
|
help="Build error field on which the regex will be applied to extract a qualifier",
|
||||||
|
)
|
||||||
|
|
||||||
|
test_ids = fields.One2many('runbot.error.qualify.test', 'qualify_regex_id', string="Test Sample", help="Error samples to test qualifying regex")
|
||||||
|
|
||||||
|
@api.constrains('regex')
|
||||||
|
def _validate(self):
|
||||||
|
for rec in self:
|
||||||
|
try:
|
||||||
|
r = re.compile(rec.regex)
|
||||||
|
except re.error as e:
|
||||||
|
raise ValidationError("Unable to compile regular expression: %s" % e)
|
||||||
|
# verify that a named group exist in the pattern
|
||||||
|
if not re.search(r'\(\?P<\w+>.+\)', r.pattern):
|
||||||
|
raise ValidationError(
|
||||||
|
"The regular expresion should contain at least one named group pattern e.g: '(?P<module>.+)'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _qualify(self, content):
|
||||||
|
self.ensure_one()
|
||||||
|
result = False
|
||||||
|
if content and self.regex:
|
||||||
|
result = re.search(self.regex, content, flags=re.MULTILINE)
|
||||||
|
return result.groupdict() if result else {}
|
||||||
|
|
||||||
|
@api.depends('regex', 'test_string')
|
||||||
|
def _compute_qualifiers(self):
|
||||||
|
for record in self:
|
||||||
|
if record.regex and record.test_string:
|
||||||
|
record.qualifiers = record._qualify(record.test_string)
|
||||||
|
else:
|
||||||
|
record.qualifiers = {}
|
||||||
|
|
||||||
|
|
||||||
|
class QualifyErrorTest(models.Model):
|
||||||
|
_name = 'runbot.error.qualify.test'
|
||||||
|
_description = 'Extended Relation between a qualify regex and a build error taken as sample'
|
||||||
|
|
||||||
|
qualify_regex_id = fields.Many2one('runbot.error.qualify.regex', required=True)
|
||||||
|
error_content_id = fields.Many2one('runbot.build.error.content', string='Build Error', required=True)
|
||||||
|
build_error_summary = fields.Char(related='error_content_id.summary')
|
||||||
|
build_error_content = fields.Text(related='error_content_id.content')
|
||||||
|
expected_result = JsonDictField('Expected Qualifiers')
|
||||||
|
result = JsonDictField('Result', compute='_compute_result')
|
||||||
|
is_matching = fields.Boolean(compute='_compute_result', default=False)
|
||||||
|
|
||||||
|
@api.depends('qualify_regex_id', 'error_content_id')
|
||||||
|
def _compute_result(self):
|
||||||
|
for record in self:
|
||||||
|
record.result = record.qualify_regex_id._qualify(record.build_error_content)
|
||||||
|
record.is_matching = record.result == record.expected_result and record.result != {}
|
||||||
|
@ -36,6 +36,9 @@ access_runbot_team_user,runbot_team_user,runbot.model_runbot_team,group_user,1,0
|
|||||||
access_runbot_error_bulk_wizard_admin,access_runbot_error_bulk_wizard_admin,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_admin,1,1,1,1
|
access_runbot_error_bulk_wizard_admin,access_runbot_error_bulk_wizard_admin,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_admin,1,1,1,1
|
||||||
access_runbot_error_bulk_wizard_manager,access_runbot_error_bulk_wizard_manager,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_error_manager,1,1,1,1
|
access_runbot_error_bulk_wizard_manager,access_runbot_error_bulk_wizard_manager,runbot.model_runbot_error_bulk_wizard,runbot.group_runbot_error_manager,1,1,1,1
|
||||||
|
|
||||||
|
runbot.access_runbot_error_qualify_regex_admin,access_runbot_error_qualify_regex_admin,runbot.model_runbot_error_qualify_regex,runbot.group_runbot_admin,1,1,1,1
|
||||||
|
runbot.access_runbot_error_qualify_test_admin, access_runbot_error_qualify_test_admin,runbot.model_runbot_error_qualify_test,runbot.group_runbot_admin,1,1,1,1
|
||||||
|
|
||||||
access_runbot_module_admin,runbot_module_admin,runbot.model_runbot_module,runbot.group_runbot_admin,1,1,1,1
|
access_runbot_module_admin,runbot_module_admin,runbot.model_runbot_module,runbot.group_runbot_admin,1,1,1,1
|
||||||
access_runbot_module_team_manager,runbot_module_team_manager,runbot.model_runbot_module,runbot.group_runbot_team_manager,1,1,1,1
|
access_runbot_module_team_manager,runbot_module_team_manager,runbot.model_runbot_module,runbot.group_runbot_team_manager,1,1,1,1
|
||||||
access_runbot_module_user,runbot_module_user,runbot.model_runbot_module,group_user,1,0,0,0
|
access_runbot_module_user,runbot_module_user,runbot.model_runbot_module,group_user,1,0,0,0
|
||||||
|
|
@ -98,6 +98,9 @@
|
|||||||
<field name="model">runbot.build.error.content</field>
|
<field name="model">runbot.build.error.content</field>
|
||||||
<field name="arch" type="xml">
|
<field name="arch" type="xml">
|
||||||
<form>
|
<form>
|
||||||
|
<header>
|
||||||
|
<button type="object" name="action_qualify" string="Qualify"/>
|
||||||
|
</header>
|
||||||
<sheet>
|
<sheet>
|
||||||
<group>
|
<group>
|
||||||
<field name="error_id"/>
|
<field name="error_id"/>
|
||||||
@ -107,6 +110,7 @@
|
|||||||
<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"/>
|
||||||
|
<field name="qualifiers" readonly="1"/>
|
||||||
</group>
|
</group>
|
||||||
<group name="infos" string="" col="2">
|
<group name="infos" string="" col="2">
|
||||||
<group>
|
<group>
|
||||||
@ -243,6 +247,7 @@
|
|||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<button name="action_find_duplicates" type="object" string="Find duplicates" display="always"/>
|
<button name="action_find_duplicates" type="object" string="Find duplicates" display="always"/>
|
||||||
|
<button name="action_qualify" string="Qualify" type="object" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager"/>
|
||||||
</header>
|
</header>
|
||||||
<field name="error_display_id" optional="show"/>
|
<field name="error_display_id" optional="show"/>
|
||||||
<field name="module_name" optional="show" readonly="1"/>
|
<field name="module_name" optional="show" readonly="1"/>
|
||||||
@ -316,6 +321,9 @@
|
|||||||
<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/>
|
||||||
|
<filter string="Qualified" name="qualified_errors" domain="[('qualifiers', '!=', False)]"/>
|
||||||
|
<filter string="Not Qualified" name="not_qualified_errors" domain="[('qualifiers', '=', False)]"/>
|
||||||
|
<separator/>
|
||||||
<filter string="Fixed" name="fixed_errors" domain="[('error_id.active', '=', False)]"/>
|
<filter string="Fixed" name="fixed_errors" domain="[('error_id.active', '=', False)]"/>
|
||||||
<filter string="Not Fixed" name="not_fixed_errors" domain="[('error_id.active', '=', True)]"/>
|
<filter string="Not Fixed" name="not_fixed_errors" domain="[('error_id.active', '=', True)]"/>
|
||||||
<separator/>
|
<separator/>
|
||||||
@ -405,5 +413,75 @@
|
|||||||
</field>
|
</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
|
<record id="build_error_qualify_regex_tree" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.error.qualify.regex.tree</field>
|
||||||
|
<field name="model">runbot.error.qualify.regex</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<tree string="Qualifying Regexes">
|
||||||
|
<header>
|
||||||
|
</header>
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="regex" readonly="1"/>
|
||||||
|
<field name="source_field" readonly="1"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="build_error_qualify_regex_form" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.error.qualify.regex.form</field>
|
||||||
|
<field name="model">runbot.error.qualify.regex</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
The regular expresion must have at least one named group pattern e.g: <code>'(?P<module>\w+)'</code>
|
||||||
|
</div>
|
||||||
|
<sheet>
|
||||||
|
<group name="Regex And Source">
|
||||||
|
<field name="regex"/>
|
||||||
|
<field name="source_field"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="test_ids">
|
||||||
|
<tree string="Test Samples" decoration-success="is_matching" decoration-danger="not is_matching">
|
||||||
|
<field name="error_content_id"/>
|
||||||
|
<field name="build_error_summary"/>
|
||||||
|
<field name="expected_result" widget="runbotjsonb"/>
|
||||||
|
<field name="result" widget="runbotjsonb" readonly="1"/>
|
||||||
|
<field name="is_matching" column_invisible="True"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="runbot_error_qualify_test_form" model="ir.ui.view">
|
||||||
|
<field name="name">runbot.error.qualify.test.form</field>
|
||||||
|
<field name="model">runbot.error.qualify.test</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group name="Error Sample">
|
||||||
|
<field name="error_content_id"/>
|
||||||
|
<field name="expected_result" widget="runbotjsonb"/>
|
||||||
|
</group>
|
||||||
|
<group name="Result">
|
||||||
|
<field name="result" widget="runbotjsonb" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="build_error_content"/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="open_view_build_error_qualify_regex_tree" model="ir.actions.act_window">
|
||||||
|
<field name="name">Build Errors Qualifying Regexes</field>
|
||||||
|
<field name="res_model">runbot.error.qualify.regex</field>
|
||||||
|
<field name="view_mode">tree,form</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
@ -37,6 +37,7 @@
|
|||||||
<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="Errors" id="runbot_menu_build_error_tree" parent="runbot_menu_manage_errors" sequence="5" 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="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="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="Errors Qualifying" id="runbot_menu_build_error_qualify_regex_tree" parent="runbot_menu_manage_errors" sequence="10" action="open_view_build_error_qualify_regex_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"/>
|
||||||
|
Loading…
Reference in New Issue
Block a user