[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:
Christophe Monniez 2024-10-25 15:34:54 +02:00 committed by Xavier-Do
parent 075e6a99b0
commit 03e1c9bed6
4 changed files with 173 additions and 0 deletions

View File

@ -9,6 +9,9 @@ from markupsafe import Markup
from werkzeug.urls import url_join
from odoo import models, fields, api
from odoo.exceptions import ValidationError, UserError
from odoo.tools import SQL
from ..fields import JsonDictField
_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')
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')
qualifiers = JsonDictField('Qualifiers', index=True)
responsible = fields.Many2one(related='error_id.responsible')
customer = fields.Many2one(related='error_id.customer')
@ -491,6 +495,17 @@ class BuildErrorContent(models.Model):
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]
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
####################
@ -546,6 +561,9 @@ class BuildErrorContent(models.Model):
'view_mode': 'tree,form'
}
def action_qualify(self):
self._qualify()
class BuildErrorTag(models.Model):
@ -618,3 +636,76 @@ class ErrorBulkWizard(models.TransientModel):
if self.chatter_comment:
for build_error in error_ids:
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 != {}

View File

@ -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_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_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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
36 access_runbot_module_ownership_admin access_runbot_module_team_manager runbot_module_ownership_admin runbot_module_team_manager runbot.model_runbot_module_ownership runbot.model_runbot_module runbot.group_runbot_admin runbot.group_runbot_team_manager 1 1 1 1
37 access_runbot_module_ownership__team_manager access_runbot_module_user runbot_module_ownership_team_manager runbot_module_user runbot.model_runbot_module_ownership runbot.model_runbot_module runbot.group_runbot_team_manager group_user 1 1 0 1 0 1 0
38 access_runbot_module_ownership_user access_runbot_module_ownership_admin runbot_module_ownership_user runbot_module_ownership_admin runbot.model_runbot_module_ownership group_user runbot.group_runbot_admin 1 0 1 0 1 0 1
39 access_runbot_module_ownership__team_manager runbot_module_ownership_team_manager runbot.model_runbot_module_ownership runbot.group_runbot_team_manager 1 1 1 1
40 access_runbot_module_ownership_user runbot_module_ownership_user runbot.model_runbot_module_ownership group_user 1 0 0 0
41 access_runbot_dashboard_admin runbot_dashboard_admin runbot.model_runbot_dashboard runbot.group_runbot_admin 1 1 1 1
42 access_runbot_dashboard_admin access_runbot_dashboard_user runbot_dashboard_admin runbot_dashboard_user runbot.model_runbot_dashboard runbot.group_runbot_admin group_user 1 1 0 1 0 1 0
43 access_runbot_dashboard_user access_runbot_dashboard_tile_admin runbot_dashboard_user runbot_dashboard_tile_admin runbot.model_runbot_dashboard runbot.model_runbot_dashboard_tile group_user runbot.group_runbot_admin 1 0 1 0 1 0 1
44 access_runbot_dashboard_tile_admin access_runbot_dashboard_tile_user runbot_dashboard_tile_admin runbot_dashboard_tile_user runbot.model_runbot_dashboard_tile runbot.group_runbot_admin group_user 1 1 0 1 0 1 0

View File

@ -98,6 +98,9 @@
<field name="model">runbot.build.error.content</field>
<field name="arch" type="xml">
<form>
<header>
<button type="object" name="action_qualify" string="Qualify"/>
</header>
<sheet>
<group>
<field name="error_id"/>
@ -107,6 +110,7 @@
<field name="module_name" readonly="1"/>
<field name="function" readonly="1"/>
<field name="file_path" readonly="1"/>
<field name="qualifiers" readonly="1"/>
</group>
<group name="infos" string="" col="2">
<group>
@ -243,6 +247,7 @@
>
<header>
<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>
<field name="error_display_id" optional="show"/>
<field name="module_name" optional="show" readonly="1"/>
@ -316,6 +321,9 @@
<filter string="Undeterministic" name="random_error" domain="[('random', '=', True)]"/>
<filter string="Deterministic" name="random_error" domain="[('random', '=', False)]"/>
<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="Not Fixed" name="not_fixed_errors" domain="[('error_id.active', '=', True)]"/>
<separator/>
@ -405,5 +413,75 @@
</field>
</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&lt;module&gt;\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>
</odoo>

View File

@ -37,6 +37,7 @@
<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 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_team_tree" parent="runbot_menu_teams" sequence="30" action="open_view_runbot_team"/>