Skip to content

Commit

Permalink
[IMP] runbot: add qualifiers on build error
Browse files Browse the repository at this point in the history
This commit adds a `qualifiers` field on build error. Its purpose is to
find similary qualified errors based on the qualified error contents.

A new tab is added on the build error form. When qualifiers are added on
the error, the similar errors are computed by searching errors having
their content similary qualifieds. When found, a button can be used to
merge those errors with the current one.

Also, a button to suggest qualifiers is added. This buttons suggest
qualifiers by assembling common qualifiers on the error content's.
  • Loading branch information
d-fence committed Feb 11, 2025
1 parent 0594c71 commit b3e337b
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 1 deletion.
50 changes: 50 additions & 0 deletions runbot/models/build_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ class BuildError(models.Model):
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.")

common_qualifiers = JsonDictField('Selection Qualifiers', help="Minimal qualifiers in common needed to link error content.")
similar_ids = fields.One2many('runbot.build.error', compute='_compute_similar_ids', string="Similar Errors", help="Similar Errors based on common qualifiers")

# Build error related data
build_error_link_ids = fields.Many2many('runbot.build.error.link', compute=_compute_related_error_content_ids('build_error_link_ids'), search=_search_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')
Expand Down Expand Up @@ -149,6 +152,19 @@ def _compute_random(self):
for record in self:
record.random = any(error.random for error in record.error_content_ids)

@api.depends('common_qualifiers')
def _compute_similar_ids(self):
for record in self:
if record.common_qualifiers:
query = SQL(
r"""SELECT error_id FROM runbot_build_error_content WHERE error_id != %s AND qualifiers @> %s""",
record.id,
json.dumps(record.common_qualifiers.dict),
)
self.env.cr.execute(query)
record.similar_ids = self.env['runbot.build.error'].browse([rec[0] for rec in self.env.cr.fetchall()])
else:
record.similar_ids = False

@api.constrains('test_tags')
def _check_test_tags(self):
Expand Down Expand Up @@ -202,6 +218,7 @@ def _merge(self, others):
if not error.team_id:
error.team_id = previous_error.team_id
previous_error.error_content_ids.write({'error_id': self})
previous_error.common_qualifiers = dict()
if not previous_error.test_tags:
previous_error.message_post(body=Markup('Error merged into %s') % error._get_form_link())
previous_error.active = False
Expand Down Expand Up @@ -245,6 +262,39 @@ def action_view_errors(self):
'target': 'current',
}

def action_infer_qualifiers(self):
for record in self:
qualifiers = defaultdict(set)
key_count = defaultdict(int)
for content in record.error_content_ids:
for key, value in content.qualifiers.dict.items():
qualifiers[key].add(value)
key_count[key] += 1
record.common_qualifiers = {k: v.pop() for k, v in qualifiers.items() if len(v) == 1 and key_count[k] == len(record.error_content_ids)}

def action_show_qualified_contents(self):
similar_content_ids = []
for record in self:
if record.common_qualifiers:
query = SQL(
r"""SELECT id FROM runbot_build_error_content WHERE error_id != %s AND qualifiers @> %s""",
record.id,
json.dumps(record.common_qualifiers.dict),
)
self.env.cr.execute(query)
similar_content_ids += [rec[0] for rec in self.env.cr.fetchall()]
return {
'type': 'ir.actions.act_window',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'runbot.build.error.content',
'domain': [('id', 'in', similar_content_ids)],
'target': 'current',
}

def action_merge_similary_qualified(self):
for record in self:
record._merge(record.similar_ids)

def action_assign(self):
teams = None
repos = None
Expand Down
52 changes: 52 additions & 0 deletions runbot/tests/test_build_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,58 @@ def test_build_error_test_tags_fixing_pr(self):
self.assertEqual([], self.BuildError._disabling_tags(build_fixing))
self.assertEqual(['-bar'], self.BuildError._disabling_tags(build_random))

def test_build_error_qualifers(self):
error_contents = self.BuildErrorContent.create(
[
{
"content": "Tour foobar_tour failed at step click_here in mode admin",
},
{
"content": "Tour foobar_tour failed at step click_here in mode demo",
},
{
"content": "Tour foobar_tour failed -> click_here",
},
]
)
self.assertEqual(len(error_contents), 3)

self.env['runbot.error.qualify.regex'].create({
"regex": r"Tour (?P<tour_name>\w+) failed at step (?P<tour_step>\w+) in mode (?P<tour_mode>\w+)"
})
error_contents._qualify()

expected_common_qualifiers = {'tour_name': 'foobar_tour', 'tour_step': 'click_here'}
self.assertEqual(error_contents[0].qualifiers.dict, {**expected_common_qualifiers, 'tour_mode': 'admin'})
self.assertEqual(error_contents[1].qualifiers.dict, {**expected_common_qualifiers, 'tour_mode': 'demo'})
self.assertEqual(error_contents[2].qualifiers.dict, dict())

self.env['runbot.error.qualify.regex'].create({
"regex": r"Tour (?P<tour_name>\w+) failed -> (?P<tour_step>\w+)"
})

error_contents._qualify()
self.assertEqual(error_contents[0].qualifiers.dict, {**expected_common_qualifiers, 'tour_mode': 'admin'})
self.assertEqual(error_contents[1].qualifiers.dict, {**expected_common_qualifiers, 'tour_mode': 'demo'})
self.assertEqual(error_contents[2].qualifiers.dict, expected_common_qualifiers)

# now let's say that we merge admin and demo errors
main_error = error_contents[0].error_id
self.assertEqual(len(main_error.error_content_ids), 1)
main_error._merge(error_contents[1].error_id)
self.assertEqual(len(main_error.error_content_ids), 2)

self.assertFalse(main_error.common_qualifiers)
main_error.action_infer_qualifiers()
self.assertEqual(main_error.common_qualifiers.dict, expected_common_qualifiers)
self.env.flush_all()
self.assertEqual(len(main_error.similar_ids), 1)
self.assertEqual(main_error.similar_ids[0], error_contents[2].error_id)
main_error.action_merge_similary_qualified()
self.assertEqual(len(main_error.error_content_ids), 3)
self.assertEqual(main_error.error_content_ids[2], error_contents[2])


def test_build_error_team_wildcards(self):
website_team = self.RunbotTeam.create({
'name': 'website_test',
Expand Down
36 changes: 35 additions & 1 deletion runbot/views/build_error_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</group>
<group>
<field name="previous_error_id" readonly="1" invisible="not previous_error_id" text-decoration-danger="True"/>
</group>
</group>
<notebook>
<page string="Builds">
<field name="unique_build_error_link_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
Expand Down Expand Up @@ -82,6 +82,40 @@
</list>
</field>
</page>
<page string="Qualifying" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager">
<div class="btn-group">
<button class="btn btn-sm btn-primary" type="object" name="action_infer_qualifiers" help="Suggest qualifiers based on content's qualifiers">
Suggest Qualifiers
</button>
<button class="btn btn-sm btn-primary" type="object" name="action_show_qualified_contents">
View corresponding contents
</button>
<button class="btn btn-sm btn-primary" type="object" name="action_merge_similary_qualified">
Merge similary qualified errors
</button>
</div>
<field name="common_qualifiers" widget="runbotjsonb"/>
<field name="similar_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
<list 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"
>
<field name="id"/>
<field name="name" optional="show" readonly="1"/>
<field name="description" 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" 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="responsible" optional="show"/>
<field name="test_tags" optional="hide"/>
<field name="fixing_pr_id" optional="hide"/>
<field name="fixing_pr_alive" optional="hide"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
Expand Down

0 comments on commit b3e337b

Please sign in to comment.