[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 xdo
parent 509c152156
commit 4bdd2e20b8
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 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 != {}

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

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="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&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> </data>
</odoo> </odoo>

View File

@ -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"/>