Skip to content

Commit

Permalink
[WIP] similary qualified contents
Browse files Browse the repository at this point in the history
Also compute the similar_content_ids.
If some error content's are similary qualified but linked to another
error, they are displayed in a notebook page. They can be re-linked to
the current error.
  • Loading branch information
d-fence committed Feb 20, 2025
1 parent 251a82a commit 0cf0bca
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 30 deletions.
116 changes: 98 additions & 18 deletions runbot/models/build_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,12 @@ 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', compute='_compute_common_qualifiers', store=True, help="Minimal qualifiers in common needed to link error content.")
common_qualifiers = JsonDictField('Common Qualifiers', compute='_compute_common_qualifiers', store=True, 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")
similar_content_ids = fields.One2many('runbot.build.error.content', compute='_compute_similar_content_ids', string="Similar Error Contents", help="Similar Error contents based on common qualifiers")
unique_qualifiers = JsonDictField('Non conflicting Qualifiers', compute='_compute_unique_qualifiers', store=True, help="Non conflicting qualifiers in common needed to link error content.")
analogous_ids = fields.One2many('runbot.build.error', compute='_compute_analogous_ids', string="Analogous Errors", help="Analogous Errors based on unique qualifiers")
analogous_content_ids= fields.One2many('runbot.build.error.content', compute='_compute_analogous_content_ids', string="Analogous Error Contents", help="Analogous Error contents based on unique 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'))
Expand Down Expand Up @@ -163,12 +167,23 @@ def _compute_common_qualifiers(self):
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)}

@api.depends('error_content_ids.qualifiers')
def _compute_unique_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.unique_qualifiers = {k: v.pop() for k, v in qualifiers.items() if len(v) == 1}

@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""",
r"""SELECT id FROM runbot_build_error WHERE id != %s AND common_qualifiers @> %s""",
record.id,
json.dumps(record.common_qualifiers.dict),
)
Expand All @@ -177,6 +192,49 @@ def _compute_similar_ids(self):
else:
record.similar_ids = False

@api.depends('common_qualifiers')
def _compute_similar_content_ids(self):
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)
record.similar_content_ids = self.env['runbot.build.error.content'].browse([rec[0] for rec in self.env.cr.fetchall()])
else:
record.similar_content_ids = False

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

@api.depends('common_qualifiers')
def _compute_analogous_content_ids(self):
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.unique_qualifiers.dict),
)
self.env.cr.execute(query)
record.analogous_content_ids = self.env['runbot.build.error.content'].browse([rec[0] for rec in self.env.cr.fetchall()])
else:
record.analogous_content_ids = False


@api.constrains('test_tags')
def _check_test_tags(self):
for build_error in self:
Expand Down Expand Up @@ -273,28 +331,49 @@ def action_view_errors(self):
'target': 'current',
}

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()]
def action_view_similary_qualified(self):
return {
'type': 'ir.actions.act_window',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'runbot.build.error',
'domain': [('id', 'in', [self.id] + self.similar_ids.ids)],
'context': {'active_test': False},
'target': 'current',
'name': 'Similary Qualified Errors'
}

def action_view_similary_qualified_contents(self):
return {
'type': 'ir.actions.act_window',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'runbot.build.error.content',
'domain': [('id', 'in', similar_content_ids)],
'domain': [('id', 'in', self.similar_content_ids.ids)],
'context': {'active_test': False},
'target': 'current',
'name': 'Similary Qualified Contents'
}

def action_merge_similary_qualified(self):
for record in self:
record._merge(record.similar_ids)
def action_view_analogous_qualified(self):
return {
'type': 'ir.actions.act_window',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'runbot.build.error',
'domain': [('id', 'in', [self.id] + self.analogous_ids.ids)],
'context': {'active_test': False},
'target': 'current',
'name': 'Similary Qualified Errors'
}

def action_view_analogous_qualified_contents(self):
return {
'type': 'ir.actions.act_window',
'views': [(False, 'list'), (False, 'form')],
'res_model': 'runbot.build.error.content',
'domain': [('id', 'in', self.analogous_content_ids.ids)],
'context': {'active_test': False},
'target': 'current',
'name': 'Similary Qualified Contents'
}

def action_assign(self):
teams = None
Expand Down Expand Up @@ -782,7 +861,8 @@ def _qualify(self, build_error_content):
result = False
if content and self.regex:
result = re.search(self.regex, content, flags=re.MULTILINE)
return result.groupdict() if result else {}
# filtering empty values to allow non mandatory named groups
return {k:v for k,v in result.groupdict().items() if v} if result else {}


class QualifyErrorTest(models.Model):
Expand Down
17 changes: 11 additions & 6 deletions runbot/tests/test_build_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,19 +541,23 @@ def test_build_error_qualifers(self):
{
"content": "Tour foobar_tour failed -> click_here",
},
{
"content": "Tour foobar_tour failed",
},
]
)
self.assertEqual(len(error_contents), 3)
self.assertEqual(len(error_contents), 4)
self.assertEqual(len(self.BuildError.search([('error_content_ids', 'in', error_contents.ids)])), 4)

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+)"
"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.assertEqual(error_contents[2].qualifiers.dict, {'tour_name': 'foobar_tour'})

self.env['runbot.error.qualify.regex'].create({
"regex": r"Tour (?P<tour_name>\w+) failed -> (?P<tour_step>\w+)"
Expand All @@ -574,10 +578,11 @@ def test_build_error_qualifers(self):
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])

# let's merge all errors and verify the qualifiers
main_error._merge(error_contents.error_id)
self.assertEqual(main_error.common_qualifiers.dict, {'tour_name': 'foobar_tour'})
self.assertEqual(main_error.unique_qualifiers.dict, {'tour_name': 'foobar_tour', 'tour_step': 'click_here'})

def test_build_error_team_wildcards(self):
website_team = self.RunbotTeam.create({
Expand Down
99 changes: 93 additions & 6 deletions runbot/views/build_error_views.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,10 @@
</list>
</field>
</page>
<page string="Qualifying" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager">
<page string="Similary qualified Errors" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager" invisible="not common_qualifiers">
<div class="btn-group">
<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 class="btn btn-sm btn-primary" type="object" name="action_view_similary_qualified">
View similary qualified errors
</button>
</div>
<field name="common_qualifiers" widget="runbotjsonb"/>
Expand All @@ -113,6 +110,96 @@
</list>
</field>
</page>
<page string="Similary qualified Error Contents" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager" invisible="not common_qualifiers">
<div class="btn-group">
<button class="btn btn-sm btn-primary" type="object" name="action_view_similary_qualified_contents">
View Similary Contents
</button>
</div>
<field name="common_qualifiers" widget="runbotjsonb"/>
<field name="similar_content_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
<list string="Error Contents"
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="error_display_id" optional="show"/>
<field name="module_name" optional="show" readonly="1"/>
<field name="summary" optional="show" 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"/>
<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"/>
<field name="fingerprint" optional="hide"/>
</list>
</field>
</page>
<page string="Analogous Errors" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager" invisible="not unique_qualifiers">
<div class="btn-group">
<button class="btn btn-sm btn-primary" type="object" name="action_view_analogous_qualified">
View analogous errors
</button>
</div>
<field name="unique_qualifiers" widget="runbotjsonb"/>
<field name="analogous_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>
<page string="Analogous Error Contents" groups="runbot.group_runbot_admin,runbot.group_runbot_error_manager" invisible="not unique_qualifiers">
<div class="btn-group">
<button class="btn btn-sm btn-primary" type="object" name="action_view_analogous_qualified_contents">
View Analogous Contents
</button>
</div>
<field name="unique_qualifiers" widget="runbotjsonb"/>
<field name="analogous_content_ids" widget="one2many" options="{'not_delete': True, 'no_create': True}" readonly="1">
<list string="Error Contents"
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="error_display_id" optional="show"/>
<field name="module_name" optional="show" readonly="1"/>
<field name="summary" optional="show" 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"/>
<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"/>
<field name="fingerprint" optional="hide"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
Expand Down

0 comments on commit 0cf0bca

Please sign in to comment.