Coverage for CIResults / models.py: 95%
1120 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 09:21 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-27 09:21 +0000
1from django.core.validators import RegexValidator, URLValidator
2from django.core.exceptions import ValidationError
3from django.conf import settings
4from django.contrib.auth import get_user_model
5from django.contrib.auth.models import User
6from django.contrib.humanize.templatetags.humanize import naturaltime
7from django.utils.functional import cached_property
8from django.db import models, transaction
9from django.utils.timesince import timesince
10from django.utils import timezone
11from django.db.models import Q, Exists, OuterRef, UniqueConstraint
12from django.template.loader import render_to_string
14from .runconfigdiff import RunConfigDiff
15from .filtering import (
16 FilterObjectBool,
17 FilterObjectDateTime,
18 FilterObjectDuration,
19 FilterObjectInteger,
20 FilterObjectJSON,
21 FilterObjectModel,
22 FilterObjectStr,
23 QueryParser,
24 QueryParserPython,
25 UserFiltrableMixin,
26)
27from .sandbox.io import Client
29from collections import defaultdict, OrderedDict
31from datetime import timedelta
32import hashlib
33import traceback
34import re
37def get_sentinel_user():
38 return get_user_model().objects.get_or_create(username='<deleted>',
39 defaults={'email': 'deleted.user@admin',
40 'last_login': timezone.now()})[0].id
43class ColoredObjectMixin:
44 color_hex = models.CharField(max_length=7, null=True, blank=True,
45 help_text="Color that should be used to represent this object, in hex format. "
46 "Use https://www.w3schools.com/colors/colors_picker.asp to pick a color.",
47 validators=[RegexValidator(
48 regex='^#[0-9a-f]{6}$',
49 message='Not a valid hex color format (example: #89ab13)',
50 )])
52 @cached_property
53 def color(self):
54 if self.color_hex is not None:
55 return self.color_hex
57 # Generate a random color
58 blake2 = hashlib.blake2b()
59 blake2.update(self.name.encode())
60 return "#" + blake2.hexdigest()[-7:-1]
63# Bug tracking
66class BugTrackerSLA(models.Model):
67 tracker = models.ForeignKey("BugTracker", on_delete=models.CASCADE)
68 priority = models.CharField(max_length=50, help_text="Name of the priority you want to define the "
69 "SLA for (case insensitive)")
70 SLA = models.DurationField(help_text="Expected SLA for this priority")
72 class Meta:
73 constraints = [
74 UniqueConstraint(fields=('tracker', 'priority'), name='unique_tracker_priority'),
75 ]
76 verbose_name_plural = "Bug Tracker SLAs"
78 def __str__(self):
79 return "{}: {} -> {}".format(str(self.tracker), self.priority, naturaltime(self.SLA))
82class Person(models.Model):
83 full_name = models.CharField(max_length=100, help_text="Full name of the person", blank=True, null=True)
84 email = models.EmailField(null=True)
86 added_on = models.DateTimeField(auto_now_add=True)
88 def __str__(self):
89 has_full_name = self.full_name is not None and len(self.full_name) > 0
90 has_email = self.email is not None and len(self.email) > 0
92 if has_full_name and has_email:
93 return "{} <{}>".format(self.full_name, self.email)
94 elif has_full_name:
95 return self.full_name
96 elif has_email:
97 return self.email
98 else:
99 return "(No name or email)"
102class BugTrackerAccount(models.Model):
103 tracker = models.ForeignKey("BugTracker", on_delete=models.CASCADE)
104 person = models.ForeignKey(Person, on_delete=models.CASCADE)
106 user_id = models.CharField(max_length=254, help_text="User ID on the bugtracker")
108 is_developer = models.BooleanField(help_text="Is this a developer's account?")
110 class Meta:
111 constraints = [
112 UniqueConstraint(fields=('tracker', 'user_id'), name='unique_tracker_user_id'),
113 ]
115 def __str__(self):
116 return str(self.person)
119class BugTracker(models.Model):
120 """ Represents a bug tracker such as Bugzilla or Jira """
122 name = models.CharField(max_length=50, unique=True, help_text="Full name of the bugtracker (e.g. Freedesktop.org)")
123 short_name = models.CharField(max_length=10, help_text="Very shorthand name (e.g. fdo)")
124 project = models.CharField(max_length=50, blank=True, null=True,
125 help_text="Specific project key in bugtracker to use (e.g. "
126 "gitlab (project id): 1234 "
127 "bugzilla (product/component): Mesa/EGL"
128 "jira (project key): TEST)")
129 separator = models.CharField(max_length=1,
130 help_text="Separator to construct a shorthand of the bug (e.g. '#' to get fdo#1234)")
131 public = models.BooleanField(help_text="Should bugs filed on this tracker be visible on the public website?")
133 url = models.URLField(help_text="Public URL to the bugtracker (e.g. "
134 "gitlab: https://gitlab.freedesktop.org "
135 "bugzilla: https://bugs.freedesktop.org)")
137 bug_base_url = models.URLField(help_text="Base URL for constructing (e.g. "
138 "gitlab: https://gitlab.freedesktop.org/cibuglog/cibuglog/issues/ "
139 "bugzilla: https://bugs.freedesktop.org/show_bug.cgi?id=)")
141 tracker_type = models.CharField(max_length=50, help_text="Legal values: bugzilla, gitlab, jira, untracked")
142 username = models.CharField(max_length=50, blank=True,
143 help_text="Username to connect to the bugtracker. "
144 "Leave empty if the bugs are public or if you do not to post comments.")
145 password = models.CharField(max_length=50, blank=True,
146 help_text="Password to connect to the bugtracker. "
147 "Leave empty if the bugs are public or if you do not to post comments.")
148 # Stats
149 polled = models.DateTimeField(null=True, blank=True,
150 help_text="Last time the bugtracker was polled. To be filled automatically.")
152 # Configurations
153 components_followed = models.TextField(null=True, blank=True,
154 help_text="Coma-separated list of components you would like to "
155 "keep track of. On Gitlab, specify the list of labels an issue "
156 "needs to have leave empty if you want all issues.")
157 components_followed_since = models.DateTimeField(blank=True, null=True,
158 help_text="Poll bugs older than this date. WARNING: "
159 "Run poll_all_bugs.py after changing this date.")
160 first_response_SLA = models.DurationField(help_text="Time given to developers to provide the first "
161 "response after its creation", default=timedelta(days=1))
162 custom_fields_map = models.JSONField(null=True, blank=True,
163 help_text="Mapping of custom_fields that should be included when polling Bugs "
164 "from this BugTracker, e.g. "
165 "{'customfield_xxxx': 'severity', 'customfield_yyyy': 'build_nro'}. "
166 "If a customfield mapping corresponds to an existing Bug field, "
167 "e.g. severity, the corresponding Bug field will be populated. "
168 "If not, e.g. build_nro, this will be populated in the Bug's "
169 "custom_fields field. (Leave empty if not using custom fields)")
171 @cached_property
172 def SLAs_cached(self):
173 slas = dict()
174 for sla_entry in BugTrackerSLA.objects.filter(tracker=self):
175 slas[sla_entry.priority.lower()] = sla_entry.SLA
176 return slas
178 @cached_property
179 def tracker(self):
180 from .bugtrackers import Untracked, Bugzilla, Jira, GitLab
182 if self.tracker_type == "bugzilla":
183 return Bugzilla(self)
184 elif self.tracker_type == "gitlab":
185 return GitLab(self)
186 elif self.tracker_type == "jira":
187 return Jira(self)
188 elif self.tracker_type == "jira_untracked" or self.tracker_type == "untracked":
189 return Untracked(self)
190 else:
191 raise ValueError("The bugtracker type '{}' is unknown".format(self.tracker_type))
193 def poll(self, bug, force_polling_comments=False):
194 self.tracker.poll(bug, force_polling_comments)
195 bug.polled = self.tracker_time
197 def poll_all(self, stop_event=None, bugs=None):
198 if bugs is None:
199 bugs = Bug.objects.filter(tracker=self)
201 for bug in bugs:
202 # Make sure the event object did not signal us to stop
203 if stop_event is not None and stop_event.is_set():
204 return
206 try:
207 self.poll(bug)
208 except Exception: # pragma: no cover
209 print("{} could not be polled:".format(bug)) # pragma: no cover
210 traceback.print_exc() # pragma: no cover
212 bug.save()
214 # We do not catch any exceptions, so if we reach this point, it means
215 # all bugs have been updated.
216 self.polled = self.tracker_time
217 self.save()
219 @property
220 def tracker_time(self):
221 return self.tracker._get_tracker_time()
223 def to_tracker_tz(self, dt):
224 return self.tracker._to_tracker_tz(dt)
226 @property
227 def open_statuses(self):
228 return self.tracker.open_statuses
230 def is_bug_open(self, bug):
231 return bug.status in self.open_statuses
233 @property
234 def components_followed_list(self):
235 if self.components_followed is None:
236 return []
237 else:
238 return [c.strip() for c in self.components_followed.split(',')]
240 @transaction.atomic
241 def get_or_create_bugs(self, ids):
242 new_bugs = set()
244 known_bugs = Bug.objects.filter(tracker=self, bug_id__in=ids)
245 known_bug_ids = set([b.bug_id for b in known_bugs])
246 for bug_id in ids - known_bug_ids:
247 new_bugs.add(Bug.objects.create(tracker=self, bug_id=bug_id))
249 return set(known_bugs).union(new_bugs)
251 def __set_tracker_to_bugs__(self, bugs):
252 for bug in bugs:
253 bug.tracker = self
254 return bugs
256 # WARNING: Some bugs may not have been polled yet
257 def open_bugs(self):
258 open_bugs = set(Bug.objects.filter(tracker=self, status__in=self.open_statuses))
260 if not self.tracker.has_components or len(self.components_followed_list) > 0:
261 open_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list,
262 status=self.open_statuses)
263 open_bugs.update(self.get_or_create_bugs(open_bug_ids))
265 return self.__set_tracker_to_bugs__(open_bugs)
267 def bugs_in_issues(self):
268 # Get the list of bugs from this tracker associated to active issues
269 bugs_in_issues = set()
270 for issue in Issue.objects.filter(archived_on=None).prefetch_related('bugs__tracker'):
271 for bug in issue.bugs.filter(tracker=self):
272 bugs_in_issues.add(bug)
274 return bugs_in_issues
276 def followed_bugs(self):
277 return self.__set_tracker_to_bugs__(self.open_bugs() | self.bugs_in_issues())
279 def updated_bugs(self):
280 if not self.polled:
281 return self.followed_bugs()
283 all_bug_ids = set(Bug.objects.filter(tracker=self).values_list('bug_id', flat=True))
285 td = self.tracker_time - timezone.now()
286 polled_time = self.to_tracker_tz(self.polled + td)
287 all_upd_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list,
288 updated_since=polled_time)
289 not_upd_ids = all_bug_ids - all_upd_bug_ids
291 open_bug_ids = set()
292 if not self.tracker.has_components or len(self.components_followed_list) > 0:
293 open_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list,
294 status=self.open_statuses)
296 open_db_bug_ids = set(Bug.objects.filter(tracker=self,
297 status__in=self.open_statuses).values_list('bug_id', flat=True))
298 issue_bugs_ids = set(map(lambda x: x.bug_id, self.bugs_in_issues()))
300 bug_ids = (open_bug_ids | open_db_bug_ids | issue_bugs_ids) - not_upd_ids
301 upd_bugs = self.get_or_create_bugs(bug_ids)
303 return self.__set_tracker_to_bugs__(upd_bugs)
305 def unreplicated_bugs(self):
306 unrep_bug_ids = set(Bug.objects.filter(~Exists(Bug.objects.filter(parent=OuterRef('pk'))),
307 parent__isnull=True,
308 tracker=self,
309 status__in=self.open_statuses).values_list('bug_id', flat=True))
311 unrep_bugs = self.get_or_create_bugs(unrep_bug_ids)
313 return self.__set_tracker_to_bugs__(unrep_bugs)
315 def clean(self):
316 if self.custom_fields_map is None:
317 return
319 for field in self.custom_fields_map:
320 if self.custom_fields_map[field] is None:
321 continue
322 self.custom_fields_map[field] = str(self.custom_fields_map[field])
324 def save(self, *args, **kwargs):
325 self.clean()
326 super().save(*args, **kwargs)
328 def __str__(self):
329 return self.name
332class Bug(models.Model, UserFiltrableMixin):
333 filter_objects_to_db = {
334 'filter_description':
335 FilterObjectStr('issue__filters__description',
336 'Description of what the filter associated to an issue referencing this bug matches'),
337 'filter_runconfig_tag_name':
338 FilterObjectStr('issue__filters__tags__name',
339 'Run configuration tag matched by the filter associated to an issue referencing this bug'),
340 'filter_machine_tag_name':
341 FilterObjectStr('issue__filters__machine_tags__name',
342 'Machine tag matched by the filter associated to an issue referencing this bug'),
343 'filter_machine_name':
344 FilterObjectStr('issue__filters__machines__name',
345 'Name of a machine matched by the filter associated to an issue referencing this bug'),
346 'filter_test_name':
347 FilterObjectStr('issue__filters__tests__name',
348 'Name of a test matched by the filter associated to an issue referencing this bug'),
349 'filter_status_name':
350 FilterObjectStr('issue__filters__statuses__name',
351 'Status matched by the filter associated to an issue referencing this bug'),
352 'filter_stdout_regex':
353 FilterObjectStr('issue__filters__stdout_regex',
354 'Standard output regex used by the filter associated to an issue referencing this bug'),
355 'filter_stderr_regex':
356 FilterObjectStr('issue__filters__stderr_regex',
357 'Standard error regex used by the filter associated to an issue referencing this bug'),
358 'filter_dmesg_regex':
359 FilterObjectStr('issue__filters__dmesg_regex',
360 'Regex for dmesg used by the filter associated to an issue referencing this bug'),
361 'filter_added_on':
362 FilterObjectDateTime('issue__issuefilterassociated__added_on',
363 'Date at which the filter was associated to the issue referencing this bug'),
364 'filter_covers_from':
365 FilterObjectDateTime('issue__issuefilterassociated__covers_from',
366 'Date of the first failure covered by the filter'),
367 'filter_deleted_on':
368 FilterObjectDateTime('issue__issuefilterassociated__deleted_on',
369 'Date at which the filter was removed from the issue referencing this bug'),
370 'filter_runconfigs_covered_count':
371 FilterObjectInteger('issue__issuefilterassociated__runconfigs_covered_count',
372 'Amount of run configurations covered by the filter associated to the issue referencing this bug'), # noqa
373 'filter_runconfigs_affected_count':
374 FilterObjectInteger('issue__issuefilterassociated__runconfigs_affected_count',
375 'Amount of run configurations affected by the filter associated to the issue referencing this bug'), # noqa
376 'filter_last_seen':
377 FilterObjectDateTime('issue__issuefilterassociated__last_seen',
378 'Date at which the filter associated to the issue referencing this bug last matched'),
379 'filter_last_seen_runconfig_name':
380 FilterObjectStr('issue__issuefilterassociated__last_seen_runconfig__name',
381 'Run configuration which last matched the filter associated to the issue referencing this bug'), # noqa
383 'issue_description': FilterObjectStr('issue__description',
384 'Free-hand text associated to the issue by the bug filer'),
385 'issue_filer_email': FilterObjectStr('issue__filer', 'Email address of the person who filed the issue'),
386 'issue_added_on': FilterObjectDateTime('issue__added_on', 'Date at which the issue was created'),
387 'issue_archived_on': FilterObjectDateTime('issue__archived_on', 'Date at which the issue was archived'),
388 'issue_expected': FilterObjectBool('issue__expected', 'Is the issue expected?'),
389 'issue_runconfigs_covered_count': FilterObjectInteger('issue__runconfigs_covered_count',
390 'Amount of run configurations covered by the issue'),
391 'issue_runconfigs_affected_count': FilterObjectInteger('issue__runconfigs_affected_count',
392 'Amount of run configurations affected by the issue'),
393 'issue_last_seen': FilterObjectDateTime('issue__last_seen', 'Date at which the issue was last seen'),
394 'issue_last_seen_runconfig_name': FilterObjectStr('issue__last_seen_runconfig__name',
395 'Run configuration which last reproduced the issue'),
397 'tracker_name': FilterObjectStr('tracker__name', 'Name of the tracker hosting the bug'),
398 'tracker_short_name': FilterObjectStr('tracker__short_name', 'Short name of the tracker which hosts the bug'),
399 'tracker_type': FilterObjectStr('tracker__tracker_type', 'Type of the tracker which hosts the bug'),
400 'bug_id': FilterObjectStr('bug_id', 'ID of the bug'),
401 'title': FilterObjectStr('title', 'Title of the bug'),
402 'created_on': FilterObjectDateTime('created', 'Date at which the bug was created'),
403 'updated_on': FilterObjectDateTime('updated', 'Date at which the bug was last updated'),
404 'closed_on': FilterObjectDateTime('closed', 'Date at which the bug was closed'),
405 'creator_name': FilterObjectStr('creator__person__full_name', 'Name of the creator of the bug'),
406 'creator_email': FilterObjectStr('creator__person__email', 'Email address of the creator of the bug'),
407 'assignee_name': FilterObjectStr('assignee__person__full_name', 'Name of the assignee of the bug'),
408 'assignee_email': FilterObjectStr('assignee__person__email', 'Email address of the assignee of the bug'),
409 'product': FilterObjectStr('product', 'Product of the bug'),
410 'component': FilterObjectStr('component', 'Component of the bug'),
411 'priority': FilterObjectStr('priority', 'Priority of the bug'),
412 'features': FilterObjectStr('features', 'Features affected (coma-separated list)'),
413 'platforms': FilterObjectStr('platforms', 'Platforms affected (coma-separated list)'),
414 'status': FilterObjectStr('status', 'Status of the bug (RESOLVED/FIXED, ...)'),
415 'tags': FilterObjectStr('tags', 'Tags/labels associated to the bug (coma-separated list)'),
416 'custom_fields': FilterObjectJSON('custom_fields', 'Custom Bug fields stored by key. Access using dot'
417 'notation, e.g. custom_fields.build_id'),
418 'parent_id': FilterObjectInteger('parent', 'ID of the parent bug from which this bug has been replicated'),
419 }
421 UPDATE_PENDING_TIMEOUT = timedelta(minutes=45)
423 """
424 Stores a single bug entry, related to :model:`CIResults.BugTracker`.
425 """
426 tracker = models.ForeignKey(BugTracker, on_delete=models.CASCADE,
427 help_text="On which tracker is the bug stored")
428 bug_id = models.CharField(max_length=20,
429 help_text="The ID of the bug (e.g. 1234)")
431 # To be updated when polling
432 parent = models.ForeignKey("self", null=True, blank=True, related_name="children", on_delete=models.CASCADE,
433 help_text="This is the parent bug, if this bug is replicated. "
434 "To be filled automatically")
435 title = models.CharField(max_length=500, null=True, blank=True,
436 help_text="Title of the bug report. To be filled automatically.")
437 description = models.TextField(null=True, blank=True, help_text="Description of the bug report. To "
438 "be filled automatically")
439 created = models.DateTimeField(null=True, blank=True,
440 help_text="Date of the creation of the bug report. To be filled automatically.")
441 updated = models.DateTimeField(null=True, blank=True,
442 help_text="Last update made to the bug report. To be filled automatically.")
443 polled = models.DateTimeField(null=True, blank=True,
444 help_text="Last time the bug was polled. To be filled automatically.")
445 closed = models.DateTimeField(null=True, blank=True,
446 help_text="Time at which the bug got closed. To be filled automatically.")
447 creator = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE, null=True, blank=True,
448 related_name="bug_creator_set", help_text="Person who wrote the initial bug report")
449 assignee = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE, null=True, blank=True,
450 related_name="bug_assignee_set", help_text="Current assignee of the bug")
451 product = models.CharField(max_length=50, null=True, blank=True,
452 help_text="Product used for the bug filing (e.g. DRI) or NULL if not "
453 "applicable. "
454 "For GitLab, this is taken from labels matching 'product: $value'. "
455 "To be filled automatically.")
456 component = models.CharField(max_length=500, null=True, blank=True,
457 help_text="Component used for the bug filing (e.g. DRM/Intel) or NULL if not "
458 "applicable. "
459 "For GitLab, this is taken from labels matching 'component: $value'. "
460 "To be filled automatically.")
461 priority = models.CharField(max_length=50, null=True, blank=True,
462 help_text="Priority of the bug. For GitLab, this is taken from labels matching "
463 "'priority::$value'. To be filled automatically.")
464 severity = models.CharField(max_length=50, null=True, blank=True,
465 help_text="Severity of the bug. For GitLab, this is taken from labels matching "
466 "'severity::$value'. To be filled automatically.")
467 features = models.CharField(max_length=500, null=True, blank=True,
468 help_text="Coma-separated list of affected features or NULL if not applicable. "
469 "For GitLab, this is taken from labels matching 'feature: $value'. "
470 "To be filled automatically.")
471 platforms = models.CharField(max_length=500, null=True, blank=True,
472 help_text="Coma-separated list of affected platforms or NULL if not applicable. "
473 "For GitLab, this is taken from labels matching 'platform: $value'. "
474 "To be filled automatically.")
475 status = models.CharField(max_length=100, null=True, blank=True,
476 help_text="Status of the bug (e.g. RESOLVED/FIXED). To be filled automatically.")
477 tags = models.TextField(null=True, blank=True,
478 help_text="Stores a comma-separated list of Bug tags/labels. "
479 "To be filled automatically")
481 # TODO: Metric on time between creation and first assignment. Metric on time between creation and component updated
483 comments_polled = models.DateTimeField(null=True, blank=True,
484 help_text="Last time the comments of the bug were polled. "
485 "To be filled automatically.")
487 flagged_as_update_pending_on = models.DateTimeField(null=True, blank=True,
488 help_text="Date at which a developer indicated their "
489 "willingness to update the bug")
490 custom_fields = models.JSONField(help_text="Mapping of customfields and values for the Bug. This field will be "
491 "automatically populated based on BugTracker.custom_fields_map field",
492 default=dict)
493 first_seen_in = models.ForeignKey("RunConfig", null=True, blank=False, on_delete=models.SET_NULL)
495 class Meta:
496 constraints = [
497 UniqueConstraint(fields=('tracker', 'bug_id'), name='unique_tracker_bug_id'),
498 UniqueConstraint(fields=('tracker', 'parent'), name='unique_tracker_parent'),
499 ]
501 rd_only_fields = ['id', 'bug_id', 'tracker_id', 'tracker', 'parent_id', 'parent']
503 @property
504 def short_name(self):
505 return "{}{}{}".format(self.tracker.short_name, self.tracker.separator, self.bug_id)
507 @property
508 def url(self):
509 return "{}{}".format(self.tracker.bug_base_url, self.bug_id)
511 @property
512 def features_list(self):
513 if self.features is not None and len(self.features) > 0:
514 return [f.strip() for f in self.features.split(',')]
515 else:
516 return []
518 @property
519 def platforms_list(self):
520 if self.platforms is not None and len(self.platforms) > 0:
521 return [p.strip() for p in self.platforms.split(',')]
522 else:
523 return []
525 @property
526 def tags_list(self):
527 if self.tags is not None and len(self.tags) > 0:
528 return [t.strip() for t in self.tags.split(',')]
529 else:
530 return []
532 @property
533 def is_open(self):
534 return self.tracker.is_bug_open(self)
536 @property
537 def has_new_comments(self):
538 return self.comments_polled is None or self.comments_polled < self.updated
540 @cached_property
541 def comments_cached(self):
542 return BugComment.objects.filter(bug=self).prefetch_related("account", "account__person")
544 @cached_property
545 def involves(self):
546 actors = defaultdict(lambda: 0)
547 actors[self.creator] += 1 # NOTE: on bugzilla, we will double count the first post
548 for comment in self.comments_cached:
549 actors[comment.account] += 1
551 sorted_actors = OrderedDict()
552 for account in sorted(actors.keys(), key=lambda k: actors[k], reverse=True):
553 sorted_actors[account] = actors[account]
555 return sorted_actors
557 def __last_updated_by__(self, is_dev):
558 last = None
559 for comment in self.comments_cached:
560 # TODO: make that if a developer wrote a new bug, he/she needs to be considered as a user
561 if comment.account.is_developer == is_dev and (last is None or comment.created_on > last):
562 last = comment.created_on
563 return last
565 @cached_property
566 def last_updated_by_user(self):
567 return self.__last_updated_by__(False)
569 @cached_property
570 def last_updated_by_developer(self):
571 return self.__last_updated_by__(True)
573 @cached_property
574 def SLA(self):
575 if self.priority is not None:
576 return self.tracker.SLAs_cached.get(self.priority.lower(), timedelta.max)
577 else:
578 return timedelta.max
580 @cached_property
581 def SLA_deadline(self):
582 if self.last_updated_by_developer is not None:
583 # We have a comment, follow the SLA of the bug
584 if self.SLA != timedelta.max:
585 return self.last_updated_by_developer + self.SLA
586 else:
587 return timezone.now() + timedelta(days=365, seconds=1)
588 else:
589 # We have not done the initial triaging, give some time for the initial response
590 return self.created + self.tracker.first_response_SLA
592 @cached_property
593 def SLA_remaining_time(self):
594 diff = self.SLA_deadline - timezone.now()
595 return timedelta(seconds=int(diff.total_seconds()))
597 @cached_property
598 def SLA_remaining_str(self):
599 rt = self.SLA_remaining_time
600 if rt < timedelta(0):
601 return str(rt)[1:] + " ago"
602 else:
603 return "in " + str(rt)
605 @cached_property
606 def effective_priority(self):
607 return -self.SLA_remaining_time / self.SLA
609 @property
610 def is_being_updated(self):
611 if self.flagged_as_update_pending_on is None:
612 return False
613 else:
614 return timezone.now() - self.flagged_as_update_pending_on < self.UPDATE_PENDING_TIMEOUT
616 @property
617 def update_pending_expires_in(self):
618 if self.flagged_as_update_pending_on is None:
619 return None
620 return (self.flagged_as_update_pending_on + self.UPDATE_PENDING_TIMEOUT) - timezone.now()
622 def clean(self):
623 if self.custom_fields is None:
624 return
626 for field, value in self.custom_fields.items():
627 if isinstance(value, dict) or isinstance(value, list) or isinstance(value, tuple):
628 raise ValueError('Values stored in custom_fields cannot be tuples, lists, dictionaries')
630 def save(self, *args, **kwargs):
631 self.clean()
632 super().save(*args, **kwargs)
634 def update_from_dict(self, upd_dict):
635 if not upd_dict:
636 return
638 for field in upd_dict:
639 # Disallow updating some critical fields
640 if field in Bug.rd_only_fields:
641 continue
643 if hasattr(self, field):
644 setattr(self, field, upd_dict[field])
646 def poll(self, force_polling_comments=False):
647 self.tracker.poll(self, force_polling_comments)
649 def add_comment(self, comment):
650 self.tracker.tracker.add_comment(self, comment)
652 def add_first_seen_in(self, issue: "Issue") -> None:
653 if self.first_seen_in is not None:
654 return
655 self.first_seen_in = (
656 RunConfig.objects
657 .filter(testsuiterun__testresult__known_failure__matched_ifa__issue=issue)
658 .order_by('added_on')
659 .first()
660 )
662 def create(self):
663 try:
664 id = self.tracker.tracker.create_bug(self)
665 except ValueError: # pragma: no cover
666 traceback.print_exc() # pragma: no cover
667 else:
668 self.bug_id = id
670 def __str__(self):
671 return "{} - {}".format(self.short_name, self.title)
674class BugComment(models.Model, UserFiltrableMixin):
675 filter_objects_to_db = {
676 'filter_description':
677 FilterObjectStr('bug__issue__filters__description',
678 'Description of what a filter associated to an issue referencing the bug on which the comment was made matches'), # noqa
679 'filter_runconfig_tag_name':
680 FilterObjectStr('bug__issue__filters__tags__name',
681 'Run configuration tag matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
682 'filter_machine_tag_name':
683 FilterObjectStr('bug__issue__filters__machine_tags__name',
684 'Machine tag matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
685 'filter_machine_name':
686 FilterObjectStr('bug__issue__filters__machines__name',
687 'Name of a machine matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
688 'filter_test_name':
689 FilterObjectStr('bug__issue__filters__tests__name',
690 'Name of a test matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
691 'filter_status_name':
692 FilterObjectStr('bug__issue__filters__statuses__name',
693 'Status matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
694 'filter_stdout_regex':
695 FilterObjectStr('bug__issue__filters__stdout_regex',
696 'Standard output regex used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
697 'filter_stderr_regex':
698 FilterObjectStr('bug__issue__filters__stderr_regex',
699 'Standard error regex used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
700 'filter_dmesg_regex':
701 FilterObjectStr('bug__issue__filters__dmesg_regex',
702 'Regex for dmesg used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa
703 'filter_added_on':
704 FilterObjectDateTime('bug__issue__issuefilterassociated__added_on',
705 'Date at which the filter was associated to the issue referencing the bug on which the comment was made'), # noqa
706 'filter_covers_from':
707 FilterObjectDateTime('bug__issue__issuefilterassociated__covers_from',
708 'Date of the first failure covered by the filter associated to the issue referencing the bug on which the comment was made'), # noqa
709 'filter_deleted_on':
710 FilterObjectDateTime('bug__issue__issuefilterassociated__deleted_on',
711 'Date at which the filter was removed from the issue referencing the bug on which the comment was made'), # noqa
712 'filter_runconfigs_covered_count':
713 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_covered_count',
714 'Amount of run configurations covered by the filter associated to the issue referencing the bug on which the comment was made'), # noqa
715 'filter_runconfigs_affected_count':
716 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_affected_count',
717 'Amount of run configurations affected by the filter associated to the issue referencing the bug on which the comment was made'), # noqa
718 'filter_last_seen':
719 FilterObjectDateTime('bug__issue__issuefilterassociated__last_seen',
720 'Date at which the filter associated to the issue referencing the bug on which the comment was made last matched'), # noqa
721 'filter_last_seen_runconfig_name':
722 FilterObjectStr('bug__issue__issuefilterassociated__last_seen_runconfig__name',
723 'Run configuration which last matched the filter associated to the issue referencing the bug on which the comment was made'), # noqa
725 'issue_description': FilterObjectStr('bug__issue__description',
726 'Free-hand text associated to the issue by the bug filer'),
727 'issue_filer_email': FilterObjectStr('bug__issue__filer', 'Email address of the person who filed the issue'),
728 'issue_added_on': FilterObjectDateTime('bug__issue__added_on', 'Date at which the issue was created'),
729 'issue_archived_on': FilterObjectDateTime('bug__issue__archived_on', 'Date at which the issue was archived'),
730 'issue_expected': FilterObjectBool('bug__issue__expected', 'Is the issue expected?'),
731 'issue_runconfigs_covered_count': FilterObjectInteger('bug__issue__runconfigs_covered_count',
732 'Amount of run configurations covered by the issue'),
733 'issue_runconfigs_affected_count': FilterObjectInteger('bug__issue__runconfigs_affected_count',
734 'Amount of run configurations affected by the issue'),
735 'issue_last_seen': FilterObjectDateTime('bug__issue__last_seen', 'Date at which the issue was last seen'),
736 'issue_last_seen_runconfig_name': FilterObjectStr('bug__issue__last_seen_runconfig__name',
737 'Run configuration which last reproduced the issue'),
739 'tracker_name': FilterObjectStr('bug__tracker__name', 'Name of the tracker hosting the bug'),
740 'tracker_short_name': FilterObjectStr('bug__tracker__short_name', 'Short name of the tracker which hosts the bug'), # noqa
741 'tracker_type': FilterObjectStr('bug__tracker__tracker_type', 'Type of the tracker which hosts the bug'),
742 'bug_id': FilterObjectStr('bug__bug_id', 'ID of the bug on which the comment was made'),
743 'bug_title': FilterObjectStr('bug__title', 'Title of the bug on which the comment was made'),
744 'bug_created_on': FilterObjectDateTime('bug__created',
745 'Date at which the bug on which the comment was made was created'),
746 'bug_updated_on': FilterObjectDateTime('bug__updated',
747 'Date at which the bug on which the comment was made was last updated'),
748 'bug_closed_on': FilterObjectDateTime('bug__closed',
749 'Date at which the bug on which the comment was made was closed'),
750 'bug_creator_name': FilterObjectStr('bug__creator__person__full_name',
751 'Name of the creator of the bug on which the comment was made'),
752 'bug_creator_email': FilterObjectStr('bug__creator__person__email',
753 'Email address of the creator of the bug on which the comment was made'),
754 'bug_assignee_name': FilterObjectStr('bug__assignee__person__full_name',
755 'Name of the assignee of the bug on which the comment was made'),
756 'bug_assignee_email': FilterObjectStr('bug__assignee__person__email',
757 'Email address of the assignee of the bug on which the comment was made'),
758 'bug_product': FilterObjectStr('bug__product', 'Product of the bug on which the comment was made'),
759 'bug_component': FilterObjectStr('bug__component', 'Component of the bug on which the comment was made'),
760 'bug_priority': FilterObjectStr('bug__priority', 'Priority of the bug on which the comment was made'),
761 'bug_features': FilterObjectStr('bug__features',
762 'Features affected (coma-separated list) in the bug on which the comment was made'), # noqa
763 'bug_platforms': FilterObjectStr('bug__platforms',
764 'Platforms affected (coma-separated list) in the bug on which the comment was made'), # noqa
765 'bug_status': FilterObjectStr('bug__status',
766 'Status of the bug (RESOLVED/FIXED, ...) on which the comment was made'),
767 'bug_tags': FilterObjectStr('bug__tags', 'Tags/labels associated to the bug (coma-separated list)'),
769 'creator_name': FilterObjectStr('account__person__full_name', 'Name of the creator of the comment'),
770 'creator_email': FilterObjectStr('account__person__email', 'Email address of the creator of the comment'),
771 'creator_is_developer': FilterObjectBool('account__is_developer', 'Is the creator of the comment a developer?'),
772 'comment_id': FilterObjectInteger('comment_id', 'The ID of the comment'),
773 'created_on': FilterObjectDateTime('created_on', 'Date at wich the comment was made')
774 }
776 bug = models.ForeignKey(Bug, on_delete=models.CASCADE)
777 account = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE)
779 comment_id = models.CharField(max_length=20, help_text="The ID of the comment")
780 url = models.URLField(null=True, blank=True)
781 created_on = models.DateTimeField()
783 class Meta:
784 constraints = [
785 UniqueConstraint(fields=('bug', 'comment_id'), name='unique_bug_comment_id'),
786 ]
788 def __str__(self):
789 return "{}'s comment by {}".format(self.bug, self.account)
792def script_validator(script):
793 try:
794 client = Client.get_or_create_instance(script)
795 except (ValueError, IOError) as e:
796 raise ValidationError("Script contains syntax errors: {}".format(e))
797 else:
798 client.shutdown()
799 return script
802class ReplicationScript(models.Model):
803 """These scripts provide a method for replicating bugs between different bugtrackers, based
804 on the user defined Python script. Further documentation on the process and API can be found
805 here - :ref:`replication-doc`
806 """
807 name = models.CharField(max_length=50, unique=True, help_text="Unique name for the script")
808 created_by = models.ForeignKey(User, on_delete=models.CASCADE, help_text="The author or last editor of the script",
809 related_name='script_creator', null=True, blank=True)
810 created_on = models.DateTimeField(auto_now_add=True,
811 help_text="Date the script was created or last updated")
812 enabled = models.BooleanField(default=False, help_text="Enable bug replication")
813 source_tracker = models.ForeignKey(BugTracker, related_name="source_rep_script", on_delete=models.CASCADE,
814 null=True, help_text="Tracker to replicate from")
815 destination_tracker = models.ForeignKey(BugTracker, related_name="dest_rep_script", on_delete=models.CASCADE,
816 null=True, help_text="Tracker to replicate to")
817 script = models.TextField(null=True, blank=True,
818 help_text="Python script to be executed", validators=[script_validator])
819 script_history = models.TextField(default='[]',
820 help_text="Stores the script edit history of the ReplicationScript model "
821 "in JSON format. The keys correspond to all the fields in the ReplicationScript "
822 "model, excluding this 'script_history' field itself.")
824 class Meta:
825 constraints = [
826 UniqueConstraint(
827 fields=('source_tracker', 'destination_tracker'),
828 name='unique_source_tracker_destination_tracker',
829 ),
830 ]
832 def __str__(self):
833 return "<replication script '{}'>".format(self.name)
836# Software
837class Component(models.Model):
838 name = models.CharField(max_length=50, unique=True)
839 description = models.TextField()
840 url = models.URLField(null=True, blank=True)
841 public = models.BooleanField(help_text="Should the component (and its builds) be visible on the public website?")
843 def __str__(self):
844 return self.name
847class Build(models.Model):
848 # Minimum information needed
849 name = models.CharField(max_length=60, unique=True)
850 component = models.ForeignKey(Component, on_delete=models.CASCADE)
851 version = models.CharField(max_length=40)
852 added_on = models.DateTimeField(auto_now=True)
854 # Allow creating an overlay over the history of the component
855 parents = models.ManyToManyField('Build', blank=True)
857 # Actual build information
858 repo_type = models.CharField(max_length=50, null=True, blank=True)
859 branch = models.CharField(max_length=50, null=True, blank=True)
860 repo = models.CharField(
861 max_length=200,
862 null=True,
863 blank=True,
864 validators=[URLValidator(schemes=["ssh", "git", "git+ssh", "http", "https", "ftp", "ftps", "rsync", "file"])],
865 )
866 upstream_url = models.URLField(null=True, blank=True)
867 parameters = models.TextField(null=True, blank=True)
868 build_log = models.TextField(null=True, blank=True)
870 @property
871 def url(self):
872 if self.upstream_url is not None:
873 return self.upstream_url
874 elif self.repo is not None:
875 return "{} @ {}".format(self.version, self.repo)
876 else:
877 return self.version
879 def __str__(self):
880 return self.name
882# Results
885class VettableObjectMixin:
886 @property
887 def vetted(self):
888 return self.vetted_on is not None
890 @transaction.atomic
891 def vet(self):
892 if self.vetted_on is not None:
893 raise ValueError('The object is already vetted')
894 self.vetted_on = timezone.now()
895 self.save()
897 @transaction.atomic
898 def suppress(self):
899 if self.vetted_on is None:
900 raise ValueError('The object is already suppressed')
901 self.vetted_on = None
902 self.save()
905class Test(VettableObjectMixin, models.Model, UserFiltrableMixin):
906 filter_objects_to_db = {
907 'name': FilterObjectStr('name', "Name of the test"),
908 'vetted_on':
909 FilterObjectDateTime('vetted_on', "Datetime at which the test was vetted. None if the test is not vetted."),
910 'added_on': FilterObjectDateTime('added_on', "Datetime at which the test was added"),
911 'first_runconfig':
912 FilterObjectStr('first_runconfig__name', "Name of the first non-temporary runconfig this test was seen in"),
913 }
914 name = models.CharField(max_length=150)
915 testsuite = models.ForeignKey('TestSuite', on_delete=models.CASCADE)
916 public = models.BooleanField(db_index=True, help_text="Should the test be visible on the public website?")
917 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True,
918 help_text="When did the test get ready for pre-merge testing?")
919 added_on = models.DateTimeField(auto_now_add=True)
920 first_runconfig = models.ForeignKey('RunConfig', db_index=True, null=True, on_delete=models.SET_NULL,
921 help_text="First non-temporary runconfig that executed this test")
923 class Meta:
924 ordering = ['name']
925 constraints = [
926 UniqueConstraint(fields=('name', 'testsuite'), name='unique_name_testsuite')
927 ]
928 permissions = [
929 ("vet_test", "Can vet a test"),
930 ("suppress_test", "Can suppress a test"),
931 ]
933 def __str__(self):
934 return "{}: {}".format(self.testsuite, self.name)
936 @property
937 def in_active_ifas(self):
938 return IssueFilterAssociated.objects.filter(deleted_on=None, filter__tests__in=[self])
940 @transaction.atomic
941 def rename(self, new_name):
942 # Get the matching test, or create it
943 new_test = Test.objects.filter(name=new_name, testsuite=self.testsuite).first()
944 if new_test is None:
945 new_test = Test.objects.create(name=new_name, testsuite=self.testsuite,
946 public=self.public)
947 else:
948 new_test.public = self.public
950 new_test.vetted_on = self.vetted_on
951 new_test.save()
953 # Now, update every active IFA
954 for ifa in self.in_active_ifas:
955 ifa.filter.tests.add(new_test)
958class MachineTag(models.Model):
959 name = models.CharField(max_length=30, unique=True)
960 description = models.TextField(help_text="Description of the objectives of the tag", blank=True, null=True)
961 public = models.BooleanField(db_index=True, help_text="Should the machine tag be visible on the public website?")
963 added_on = models.DateTimeField(auto_now_add=True)
965 class Meta:
966 ordering = ['name']
968 @cached_property
969 def machines(self):
970 return sorted(Machine.objects.filter(tags__in=[self]), key=lambda m: m.name)
972 def __str__(self):
973 return self.name
976class Machine(VettableObjectMixin, ColoredObjectMixin, models.Model, UserFiltrableMixin):
977 filter_objects_to_db = {
978 'name': FilterObjectStr('name', "Name of the machine"),
979 'description': FilterObjectStr('description', "Description of the machine"),
980 'vetted_on':
981 FilterObjectDateTime('vetted_on',
982 "Datetime at which the machine was vetted. None if the machine is not vetted"),
983 'added_on': FilterObjectDateTime('added_on', "Datetime at which the machine was added"),
984 'aliases': FilterObjectStr('aliases__name', "Machine group this machine is a part of"),
985 'tags': FilterObjectStr('tags__name', "List of tags associated to this machine"),
986 }
987 name = models.CharField(max_length=100, unique=True)
988 description = models.TextField(help_text="Description of the machine", blank=True, null=True)
990 public = models.BooleanField(db_index=True, help_text="Should the machine be visible on the public website?")
992 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True,
993 help_text="When did the machine get ready for pre-merge testing?")
995 added_on = models.DateTimeField(auto_now_add=True)
997 aliases = models.ForeignKey("Machine", on_delete=models.CASCADE, null=True, blank=True,
998 help_text="This machine is an alias of another machine. "
999 "The aliased machine will be used when comparing runconfigs. "
1000 "This is useful if you have multiple identical machines that "
1001 "execute a different subset of test every run")
1003 tags = models.ManyToManyField(MachineTag, blank=True)
1005 color_hex = ColoredObjectMixin.color_hex
1007 class Meta:
1008 ordering = ['name']
1009 permissions = [
1010 ("vet_machine", "Can vet a machine"),
1011 ("suppress_machine", "Can suppress a machine"),
1012 ]
1014 @cached_property
1015 def tags_cached(self):
1016 return self.tags.all()
1018 def __str__(self):
1019 return self.name
1022class RunConfigTag(models.Model):
1023 name = models.CharField(max_length=50, unique=True,
1024 help_text="Unique name for the tag")
1025 description = models.TextField(help_text="Description of the objectives of the tag")
1026 url = models.URLField(null=True, blank=True, help_text="URL to more explanations (optional)")
1027 public = models.BooleanField(help_text="Should the tag be visible on the public website?")
1029 def __str__(self):
1030 return self.name
1033class RunConfig(models.Model):
1034 filter_objects_to_db = {
1035 'name': FilterObjectStr('name', 'Name of the run configuration'),
1036 'tag': FilterObjectStr('tags__name', 'Tag associated with the configuration for this run'),
1037 'added_on': FilterObjectDateTime('added_on', 'Date at which the run configuration got created'),
1038 'temporary': FilterObjectBool('temporary', 'Is the run configuration temporary (pre-merge testing)?'),
1039 'build': FilterObjectStr('builds__name', 'Tag associated with the configuration for this run'),
1040 'environment': FilterObjectStr('environment', 'Free-text field describing the environment of the machine'),
1042 # Through reverse accessors
1043 'machine_name': FilterObjectStr('testsuiterun__machine__name', 'Name of the machine used in this run'),
1044 'machine_tag': FilterObjectStr('testsuiterun__machine__tags__name',
1045 'Tag associated to the machine used in this run'),
1046 }
1048 name = models.CharField(max_length=70, unique=True)
1049 tags = models.ManyToManyField(RunConfigTag)
1050 temporary = models.BooleanField(help_text="This runconfig is temporary and should not be part of statistics")
1051 url = models.URLField(null=True, blank=True)
1053 added_on = models.DateTimeField(auto_now_add=True, db_index=True)
1055 builds = models.ManyToManyField(Build)
1057 environment = models.TextField(null=True, blank=True,
1058 help_text="A human-readable, and machine-parsable definition of the environment. "
1059 "Make sure the environment contains a header with the format and version.")
1061 @cached_property
1062 def tags_cached(self):
1063 return self.tags.all()
1065 @cached_property
1066 def tags_ids_cached(self):
1067 return set([t.id for t in self.tags_cached])
1069 @cached_property
1070 def builds_cached(self):
1071 return self.builds.all()
1073 @cached_property
1074 def builds_ids_cached(self):
1075 return set([b.id for b in self.builds_cached])
1077 @cached_property
1078 def public(self):
1079 for tag in self.tags_cached:
1080 if not tag.public:
1081 return False
1082 return True
1084 @cached_property
1085 def runcfg_history(self):
1086 # TODO: we may want to use something else but the tags to find out
1087 # the history of this particular run config
1089 # TODO 2: make sure the tags sets are equal, not just that a set is inside
1090 # another one. This is a regression caused by django 2.0
1091 tags = self.tags_cached
1092 return RunConfig.objects.order_by("-added_on").filter(tags__in=tags, temporary=False)
1094 @cached_property
1095 def runcfg_history_offset(self):
1096 for i, runcfg in enumerate(self.runcfg_history):
1097 if self.id == runcfg.id:
1098 return i
1099 raise ValueError("BUG: The runconfig ID has not been found in the runconfig history")
1101 def __str__(self):
1102 return self.name
1104 def update_statistics(self):
1105 stats = []
1107 # Do not compute statistics for temporary runconfigs
1108 if self.temporary:
1109 return stats
1111 ifas = IssueFilterAssociated.objects_ready_for_matching.filter(Q(deleted_on=None))
1113 # Check if all filters cover and/or match results. De-dupplicate filters first
1114 filters = set([e.filter for e in ifas])
1115 for filter in filters:
1116 fs = RunFilterStatistic(filter=filter, runconfig=self, covered_count=0,
1117 matched_count=0)
1119 fs.covered_count = filter.covered_results.count()
1120 if fs.covered_count < 1:
1121 continue
1122 matched_failures = [result for result in filter.matched_results if result.is_failure]
1123 fs.matched_count = len(matched_failures)
1124 stats.append(fs)
1126 # Remove all the stats for the current run, and add the new ones
1127 with transaction.atomic():
1128 RunFilterStatistic.objects.filter(runconfig=self, filter__in=filters).delete()
1129 RunFilterStatistic.objects.bulk_create(stats)
1131 return stats
1133 def compare(self, to, max_missing_hosts=0.5, no_compress=False, query=None):
1134 return RunConfigDiff(self, to, max_missing_hosts=max_missing_hosts,
1135 no_compress=no_compress, query=query)
1138class TestSuite(VettableObjectMixin, Component):
1139 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True,
1140 help_text="When did the testsuite get ready for pre-merge testing?")
1142 # List of results you do not want to file bugs for
1143 acceptable_statuses = models.ManyToManyField('TextStatus', related_name='+', blank=True)
1145 # Status to ignore for diffing
1146 notrun_status = models.ForeignKey('TextStatus', null=True, blank=True, on_delete=models.SET_NULL,
1147 related_name='+')
1149 class Meta:
1150 permissions = [
1151 ("vet_testsuite", "Can vet a testsuite"),
1152 ("suppress_testsuite", "Can suppress a testsuite"),
1153 ]
1155 @cached_property
1156 def __acceptable_statuses__(self):
1157 return set([r.id for r in self.acceptable_statuses.all()])
1159 def __str__(self):
1160 return self.name
1162 def is_failure(self, status):
1163 return status.id not in self.__acceptable_statuses__
1166class TestsuiteRun(models.Model, UserFiltrableMixin):
1167 # For the FilterMixin.
1168 filter_objects_to_db = {
1169 'testsuite_name': FilterObjectStr('testsuite__name',
1170 'Name of the testsuite that was used for this run'),
1171 'runconfig': FilterObjectModel(RunConfig, 'runconfig', 'Run configuration the test is part of'),
1172 'runconfig_name': FilterObjectStr('runconfig__name', 'Name of the run configuration'),
1173 'runconfig_tag': FilterObjectStr('runconfig__tags__name',
1174 'Tag associated with the configuration for this run'),
1175 'runconfig_added_on': FilterObjectDateTime('runconfig__added_on',
1176 'Date at which the run configuration got created'),
1177 'runconfig_temporary': FilterObjectBool('runconfig__temporary',
1178 'Is the run configuration temporary (pre-merge testing)?'),
1179 'machine_name': FilterObjectStr('machine__name', 'Name of the machine used in this run'),
1180 'machine_tag': FilterObjectStr('machine__tags__name', 'Tag associated to the machine used in this run'),
1181 'url': FilterObjectStr('url', 'External URL associated to this testsuite run'),
1182 'start': FilterObjectDateTime('start', "Local time at witch the run started on the machine"),
1183 'duration': FilterObjectDuration('duration', 'Duration of the testsuite run'),
1184 'reported_on': FilterObjectDateTime('reported_on',
1185 'Date at which the testsuite run got imported in CI Bug Log'),
1186 'environment': FilterObjectStr('environment', 'Free-text field describing the environment of the machine'),
1187 'log': FilterObjectStr('log', 'Log of the testsuite run'),
1188 }
1190 testsuite = models.ForeignKey(TestSuite, on_delete=models.CASCADE)
1191 runconfig = models.ForeignKey(RunConfig, on_delete=models.CASCADE)
1192 machine = models.ForeignKey(Machine, on_delete=models.CASCADE)
1193 run_id = models.IntegerField()
1194 url = models.URLField(null=True, blank=True)
1196 start = models.DateTimeField()
1197 duration = models.DurationField()
1198 reported_on = models.DateTimeField(auto_now_add=True)
1200 environment = models.TextField(blank=True,
1201 help_text="A human-readable, and machine-parsable definition of the environment. "
1202 "Make sure the environment contains a header with the format and version.")
1203 log = models.TextField(blank=True)
1205 class Meta:
1206 constraints = [
1207 UniqueConstraint(
1208 fields=('testsuite', 'runconfig', 'machine', 'run_id'),
1209 name='unique_testsuite_runconfig_machine_run_id',
1210 ),
1211 ]
1212 ordering = ['start']
1214 def __str__(self):
1215 return "{} on {} - testsuite run {}".format(self.runconfig.name, self.machine.name, self.run_id)
1218class TextStatus(VettableObjectMixin, ColoredObjectMixin, models.Model, UserFiltrableMixin):
1219 filter_objects_to_db = {
1220 'name': FilterObjectStr('name', "Name of the status"),
1221 'added_on': FilterObjectDateTime('added_on', "Datetime at which the text status was added"),
1222 }
1223 testsuite = models.ForeignKey(TestSuite, on_delete=models.CASCADE)
1224 name = models.CharField(max_length=20)
1226 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True,
1227 help_text="When did the status get ready for pre-merge testing?")
1228 added_on = models.DateTimeField(auto_now_add=True)
1230 color_hex = ColoredObjectMixin.color_hex
1232 severity = models.PositiveIntegerField(null=True, blank=True,
1233 help_text="Define how bad a the status is, from better to worse. "
1234 "The best possible is 0.")
1236 class Meta:
1237 constraints = [
1238 UniqueConstraint(fields=('testsuite', 'name'), name='unique_testsuite_name')
1239 ]
1240 verbose_name_plural = "Text Statuses"
1241 permissions = [
1242 ("vet_textstatus", "Can vet a text status"),
1243 ("suppress_textstatus", "Can suppress a text status"),
1244 ]
1246 @property
1247 def is_failure(self):
1248 return self.testsuite.is_failure(self)
1250 @property
1251 def is_notrun(self):
1252 return self == self.testsuite.notrun_status
1254 @property
1255 def actual_severity(self):
1256 if self.severity is not None:
1257 return self.severity
1258 elif self.is_notrun:
1259 return 0
1260 elif not self.is_failure:
1261 return 1
1262 else:
1263 return 2
1265 def __str__(self):
1266 return "{}: {}".format(self.testsuite, self.name)
1269class TestResultAssociatedManager(models.Manager):
1270 def get_queryset(self):
1271 return super().get_queryset().prefetch_related('status__testsuite__acceptable_statuses',
1272 'status', 'ts_run__machine',
1273 'ts_run__machine__tags',
1274 'ts_run__runconfig__tags',
1275 'test')
1278class TestResult(models.Model, UserFiltrableMixin):
1279 # For the FilterMixin.
1280 filter_objects_to_db = {
1281 'runconfig': FilterObjectModel(RunConfig, 'ts_run__runconfig', 'Run configuration the test is part of'),
1282 'runconfig_name': FilterObjectStr('ts_run__runconfig__name', 'Name of the run configuration'),
1283 'runconfig_tag': FilterObjectStr('ts_run__runconfig__tags__name',
1284 'Tag associated with the configuration used for this test execution'),
1285 'runconfig_added_on': FilterObjectDateTime('ts_run__runconfig__added_on',
1286 'Date at which the run configuration got created'),
1287 'runconfig_temporary': FilterObjectBool('ts_run__runconfig__temporary',
1288 'Is the run configuration temporary, like for pre-merge testing?'),
1289 'build_name': FilterObjectStr('ts_run__runconfig__builds__name',
1290 'Name of the build for a component used for this test execution'),
1291 'build_added_on': FilterObjectDateTime('ts_run__runconfig__builds__added_on',
1292 'Date at which the build was added'),
1293 'component_name': FilterObjectStr('ts_run__runconfig__builds__component__name',
1294 'Name of a component used for this test execution'),
1295 'machine_name': FilterObjectStr('ts_run__machine__name', 'Name of the machine used for this result'),
1296 'machine_tag': FilterObjectStr('ts_run__machine__tags__name',
1297 'Tag associated to the machine used in this run'),
1298 'status_name': FilterObjectStr('status__name', 'Name of the resulting status (pass/fail/crash/...)'),
1299 'testsuite_name': FilterObjectStr('status__testsuite__name',
1300 'Name of the testsuite that contains this test'),
1301 'test_name': FilterObjectStr('test__name', 'Name of the test'),
1302 'test_added_on': FilterObjectDateTime('test__added_on', 'Date at which the test got added'),
1303 'manually_filed_on': FilterObjectDateTime('known_failure__manually_associated_on',
1304 'Date at which the failure got manually associated to an issue'),
1305 'ifa_id': FilterObjectInteger('known_failure__matched_ifa_id',
1306 'ID of the associated filter that matched the failure'),
1307 'issue_id': FilterObjectInteger('known_failure__matched_ifa__issue_id',
1308 'ID of the issue associated to the failure'),
1309 'issue_expected': FilterObjectBool('known_failure__matched_ifa__issue__expected',
1310 'Is the issue associated to the failure marked as expected?'),
1311 'filter_description': FilterObjectStr('known_failure__matched_ifa__issue__filters__description',
1312 'Description of what the filter associated to the failure'),
1313 'filter_runconfig_tag_name':
1314 FilterObjectStr('known_failure__matched_ifa__issue__filters__tags__name',
1315 'Run configuration tag matched by the filter associated to the failure'),
1316 'filter_machine_tag_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machine_tags__name',
1317 'Machine tag matched by the filter associated to the failure'),
1318 'filter_machine_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machines__name',
1319 'Name of a machine matched by the filter associated to the failure'),
1320 'filter_test_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__tests__name',
1321 'Name of a test matched by the filter associated to the failure'),
1322 'filter_status_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__statuses__name',
1323 'Status matched by the filter associated to the failure'),
1324 'filter_stdout_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stdout_regex',
1325 'Standard output regex used by the filter associated to the failure'),
1326 'filter_stderr_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stderr_regex',
1327 'Standard error regex used by the filter associated to the failure'),
1328 'filter_dmesg_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__dmesg_regex',
1329 'Regex for dmesg used by the filter associated to the failure'),
1330 'filter_added_on': FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__added_on',
1331 'Date at which the filter associated to the failure was added on to its issue'), # noqa
1332 'filter_covers_from':
1333 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__covers_from',
1334 'Date of the first failure covered by the filter associated to the failure'),
1335 'filter_deleted_on':
1336 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__deleted_on',
1337 'Date at which the filter was removed from the issue associated to the failure'),
1338 'filter_runconfigs_covered_count':
1339 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_covered_count',
1340 'Amount of run configurations covered by the filter associated to the failure'),
1341 'filter_runconfigs_affected_count':
1342 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_affected_count',
1343 'Amount of run configurations affected by the filter associated to the failure'),
1344 'filter_last_seen':
1345 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__last_seen',
1346 'Date at which the filter matching this failure was last seen'),
1347 'filter_last_seen_runconfig_name':
1348 FilterObjectStr('known_failure__matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name',
1349 'Run configuration which last matched the filter associated to the failure'),
1350 'bug_tracker_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__name',
1351 'Name of the tracker which holds the bug associated to this failure'),
1352 'bug_tracker_short_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__short_name',
1353 'Short name of the tracker which holds the bug associated to this failure'), # noqa
1354 'bug_tracker_type': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__tracker_type',
1355 'Type of the tracker which holds the bug associated to this failure'),
1356 'bug_id': FilterObjectStr('known_failure__matched_ifa__issue__bugs__bug_id',
1357 'ID of the bug associated to this failure'),
1358 'bug_title': FilterObjectStr('known_failure__matched_ifa__issue__bugs__title',
1359 'Title of the bug associated to this failure'),
1360 'bug_created_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__created',
1361 'Date at which the bug associated to this failure was created'),
1362 'bug_closed_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__closed',
1363 'Date at which the bug associated to this failure was closed'),
1364 'bug_creator_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__full_name',
1365 'Name of the creator of the bug associated to this failure'),
1366 'bug_creator_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__email',
1367 'Email address of the creator of the bug associated to this failure'),
1368 'bug_assignee_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__full_name',
1369 'Name of the assignee of the bug associated to this failure'),
1370 'bug_assignee_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__email',
1371 'Email address of the assignee of the bug associated to this failure'),
1372 'bug_product': FilterObjectStr('known_failure__matched_ifa__issue__bugs__product',
1373 'Product of the bug associated to this failure'),
1374 'bug_component': FilterObjectStr('known_failure__matched_ifa__issue__bugs__component',
1375 'Component of the bug associated to this failure'),
1376 'bug_priority': FilterObjectStr('known_failure__matched_ifa__issue__bugs__priority',
1377 'Priority of the bug associated to this failure'),
1378 'bug_features': FilterObjectStr('known_failure__matched_ifa__issue__bugs__features',
1379 'Features of the bug associated to this failure (coma-separated list)'),
1380 'bug_platforms': FilterObjectStr('known_failure__matched_ifa__issue__bugs__platforms',
1381 'Platforms of the bug associated to this failure (coma-separated list)'),
1382 'bug_status': FilterObjectStr('known_failure__matched_ifa__issue__bugs__status',
1383 'Status of the bug associated to this failure'),
1384 'bug_tags': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tags',
1385 'Tags/labels on the bug associated to this failure (coma-separated list)'),
1386 'url': FilterObjectStr('url', 'External URL of this test result'),
1387 'start': FilterObjectDateTime('start', 'Date at which this test started being executed'),
1388 'duration': FilterObjectDuration('duration', 'Time it took to execute the test'),
1389 'command': FilterObjectStr('command', 'Command used to execute the test'),
1390 'stdout': FilterObjectStr('stdout', 'Standard output of the test execution'),
1391 'stderr': FilterObjectStr('stderr', 'Error output of the test execution'),
1392 'dmesg': FilterObjectStr('dmesg', 'Kernel logs of the test execution'),
1393 }
1395 test = models.ForeignKey(Test, on_delete=models.CASCADE)
1396 ts_run = models.ForeignKey(TestsuiteRun, on_delete=models.CASCADE)
1397 status = models.ForeignKey(TextStatus, on_delete=models.CASCADE)
1399 url = models.URLField(null=True, blank=True, max_length=300)
1401 start = models.DateTimeField()
1402 duration = models.DurationField()
1404 command = models.CharField(max_length=500)
1405 stdout = models.TextField(null=True)
1406 stderr = models.TextField(null=True)
1407 dmesg = models.TextField(null=True)
1409 objects = models.Manager()
1410 objects_ready_for_matching = TestResultAssociatedManager()
1412 @cached_property
1413 def is_failure(self):
1414 return self.status.testsuite.is_failure(self.status)
1416 @cached_property
1417 def known_failures_cached(self):
1418 return self.known_failures.all()
1420 def __str__(self):
1421 return "{} on {} - {}: ({})".format(self.ts_run.runconfig.name, self.ts_run.machine.name,
1422 self.test.name, self.status)
1424# TODO: Support benchmarks too by creating BenchmarkResult (test, run, environment, ...)
1426# Issues
1429class IssueFilter(models.Model):
1430 description = models.CharField(max_length=255,
1431 help_text="Short description of what the filter matches!")
1433 tags = models.ManyToManyField(RunConfigTag, blank=True,
1434 help_text="The result's run should have at least one of these tags "
1435 "(leave empty to ignore tags)")
1436 machine_tags = models.ManyToManyField(MachineTag, blank=True,
1437 help_text="The result's machine should have one of these tags "
1438 "(leave empty to ignore machines)")
1439 machines = models.ManyToManyField(Machine, blank=True,
1440 help_text="The result's machine should be one of these machines "
1441 "(extends the set of machines selected by the machine tags, "
1442 "leave empty to ignore machines)")
1443 tests = models.ManyToManyField(Test, blank=True,
1444 help_text="The result's machine should be one of these tests "
1445 "(leave empty to ignore tests)")
1446 statuses = models.ManyToManyField(TextStatus, blank=True,
1447 help_text="The result's status should be one of these (leave empty to "
1448 "ignore results)")
1450 stdout_regex = models.CharField(max_length=1000, blank=True,
1451 help_text="The result's stdout field must contain a substring matching this "
1452 "regular expression (leave empty to ignore stdout)")
1453 stderr_regex = models.CharField(max_length=1000, blank=True,
1454 help_text="The result's stderr field must contain a substring matching this "
1455 "regular expression (leave empty to ignore stderr)")
1456 dmesg_regex = models.CharField(max_length=1000, blank=True,
1457 help_text="The result's dmesg field must contain a substring matching this "
1458 "regular expression (leave empty to ignore dmesg)")
1460 added_on = models.DateTimeField(auto_now_add=True)
1461 hidden = models.BooleanField(default=False, db_index=True, help_text="Do not show this filter in filter lists")
1462 user_query = models.TextField(blank=True, null=True, help_text="User query representation of filter")
1464 def delete(self):
1465 self.hidden = True
1467 @cached_property
1468 def tags_cached(self):
1469 return set(self.tags.all())
1471 @cached_property
1472 def tags_ids_cached(self):
1473 return set([t.id for t in self.tags_cached])
1475 @cached_property
1476 def __machines_cached__(self):
1477 return set(self.machines.all())
1479 @cached_property
1480 def __machine_tags_cached__(self):
1481 return set(self.machine_tags.all())
1483 @cached_property
1484 def machines_cached(self):
1485 machines = self.__machines_cached__.copy()
1486 for machine in Machine.objects.filter(tags__in=self.__machine_tags_cached__):
1487 machines.add(machine)
1488 return machines
1490 @cached_property
1491 def machines_ids_cached(self):
1492 return set([m.id for m in self.machines_cached])
1494 @cached_property
1495 def tests_cached(self):
1496 return set(self.tests.all())
1498 @cached_property
1499 def tests_ids_cached(self):
1500 return set([m.id for m in self.tests_cached])
1502 @cached_property
1503 def statuses_cached(self):
1504 return set(self.statuses.all())
1506 @cached_property
1507 def statuses_ids_cached(self):
1508 return set([s.id for s in self.statuses_cached])
1510 @cached_property
1511 def stdout_regex_cached(self):
1512 return re.compile(self.stdout_regex, re.DOTALL)
1514 @cached_property
1515 def stderr_regex_cached(self):
1516 return re.compile(self.stderr_regex, re.DOTALL)
1518 @cached_property
1519 def dmesg_regex_cached(self):
1520 return re.compile(self.dmesg_regex, re.DOTALL)
1522 @cached_property
1523 def covered_results(self):
1524 return QueryParser(
1525 TestResult, self.equivalent_user_query, ignore_fields=["stdout", "stderr", "dmesg", "status_name"]
1526 ).objects
1528 @cached_property
1529 def __covers_function(self):
1530 parser = QueryParserPython(
1531 TestResult, self.equivalent_user_query, ignore_fields=["stdout", "stderr", "dmesg", "status_name"]
1532 )
1533 if not parser.is_valid:
1534 raise ValueError("Invalid cover function", parser.error)
1535 return parser.matching_fn
1537 def covers(self, result):
1538 try:
1539 return self.__covers_function(result)
1540 except ValueError as err:
1541 print(f"Couldn't cover issue filter {self.pk} for result {result}: {err}")
1542 return False
1544 @cached_property
1545 def matched_results(self):
1546 return QueryParser(TestResult, self.equivalent_user_query).objects
1548 @property
1549 def matched_unknown_failures(self):
1550 return QueryParser(UnknownFailure, self.equivalent_user_query).objects
1552 @cached_property
1553 def __matches_function(self):
1554 parser = QueryParserPython(TestResult, self.equivalent_user_query)
1555 if not parser.is_valid:
1556 raise ValueError("Invalid match function", parser.error)
1557 return parser.matching_fn
1559 def matches(self, result, skip_cover_test=False):
1560 try:
1561 return self.__matches_function(result)
1562 except ValueError as err:
1563 print(f"Couldn't match issue filter {self.pk} for result {result}: {err}")
1564 return False
1566 @transaction.atomic
1567 def replace(self, new_filter, user):
1568 # Go through all the issues that currently use this filter
1569 for e in IssueFilterAssociated.objects.filter(deleted_on=None, filter=self):
1570 e.issue.replace_filter(self, new_filter, user)
1572 # Hide this filter now and only keep it for archive purposes
1573 self.delete()
1575 def _to_user_query(self, covers=True, matches=True):
1576 query = []
1578 if covers:
1579 if len(self.tags_cached) > 0:
1580 query.append('runconfig_tag IS IN ["{}"]'.format('", "'.join([t.name for t in self.tags_cached])))
1582 if len(self.__machines_cached__) > 0 or len(self.__machine_tags_cached__) > 0:
1583 if len(self.__machines_cached__) > 0:
1584 machines = [m.name for m in self.__machines_cached__]
1585 machines_query = 'machine_name IS IN ["{}"]'.format('", "'.join(machines))
1586 if len(self.__machine_tags_cached__) > 0:
1587 tags = [t.name for t in self.__machine_tags_cached__]
1588 machine_tags_query = 'machine_tag IS IN ["{}"]'.format('", "'.join(tags))
1590 if len(self.__machines_cached__) > 0 and len(self.__machine_tags_cached__) > 0:
1591 query.append("({} OR {})".format(machines_query, machine_tags_query))
1592 elif len(self.__machines_cached__) > 0:
1593 query.append(machines_query)
1594 else:
1595 query.append(machine_tags_query)
1597 if len(self.tests_cached) > 0:
1598 tests_query = []
1600 # group the tests by testsuite
1601 testsuites = defaultdict(set)
1602 for test in self.tests_cached:
1603 testsuites[test.testsuite].add(test)
1605 # create the sub-queries
1606 for testsuite in testsuites:
1607 subquery = '(testsuite_name = "{}" AND test_name IS IN ["{}"])'
1608 tests_query.append(subquery.format(testsuite.name,
1609 '", "'.join([t.name for t in testsuites[testsuite]])))
1610 query.append("({})".format(" OR ".join(tests_query)))
1612 if matches:
1613 if len(self.statuses_cached) > 0:
1614 status_query = []
1616 # group the statuses by testsuite
1617 testsuites = defaultdict(set)
1618 for status in self.statuses_cached:
1619 testsuites[status.testsuite].add(status)
1621 # create the sub-queries
1622 for testsuite in testsuites:
1623 subquery = '(testsuite_name = "{}" AND status_name IS IN ["{}"])'
1624 status_query.append(subquery.format(testsuite.name,
1625 '", "'.join([s.name for s in testsuites[testsuite]])))
1626 query.append("({})".format(" OR ".join(status_query)))
1628 if len(self.stdout_regex) > 0:
1629 query.append("stdout ~= '{}'".format(self.stdout_regex.replace("'", "\\'")))
1631 if len(self.stderr_regex) > 0:
1632 query.append("stderr ~= '{}'".format(self.stderr_regex.replace("'", "\\'")))
1634 if len(self.dmesg_regex) > 0:
1635 query.append("dmesg ~= '{}'".format(self.dmesg_regex.replace("'", "\\'")))
1637 return " AND ".join(query)
1639 @cached_property
1640 def equivalent_user_query(self) -> str:
1641 if self.user_query:
1642 return self.user_query
1643 return self._to_user_query()
1645 def __str__(self):
1646 return self.description
1649class Rate:
1650 def __init__(self, type_str, affected, total):
1651 self._type_str = type_str
1652 self._affected = affected
1653 self._total = total
1655 @property
1656 def rate(self):
1657 if self._total > 0:
1658 return self._affected / self._total
1659 else:
1660 return 0
1662 def __str__(self):
1663 return "{} / {} {} ({:.1f}%)".format(self._affected,
1664 self._total,
1665 self._type_str,
1666 self.rate * 100.0)
1669class IssueFilterAssociatedManager(models.Manager):
1670 def get_queryset(self):
1671 return super().get_queryset().prefetch_related('filter__tags',
1672 'filter__machine_tags',
1673 'filter__machines',
1674 'filter__tests',
1675 'filter__statuses',
1676 'filter')
1679class IssueFilterAssociated(models.Model):
1680 filter = models.ForeignKey(IssueFilter, on_delete=models.CASCADE)
1681 issue = models.ForeignKey('Issue', on_delete=models.CASCADE)
1683 added_on = models.DateTimeField(auto_now_add=True)
1684 added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='filter_creator',
1685 null=True, on_delete=models.SET(get_sentinel_user))
1687 # WARNING: Make sure this is set when archiving the issue
1688 deleted_on = models.DateTimeField(blank=True, null=True, db_index=True)
1689 deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='filter_deleter',
1690 null=True, on_delete=models.SET(get_sentinel_user))
1692 # Statistics cache
1693 covers_from = models.DateTimeField(default=timezone.now)
1694 runconfigs_covered_count = models.PositiveIntegerField(default=0)
1695 runconfigs_affected_count = models.PositiveIntegerField(default=0)
1696 last_seen = models.DateTimeField(null=True, blank=True)
1697 last_seen_runconfig = models.ForeignKey('RunConfig', null=True, blank=True, on_delete=models.SET_NULL)
1699 objects = models.Manager()
1700 objects_ready_for_matching = IssueFilterAssociatedManager()
1702 @property
1703 def active(self):
1704 return self.deleted_on is None
1706 def delete(self, user, now=None):
1707 if self.deleted_on is not None:
1708 return
1710 if now is not None:
1711 self.deleted_on = now
1712 else:
1713 self.deleted_on = timezone.now()
1715 self.deleted_by = user
1716 self.save()
1718 @cached_property
1719 def __runfilter_stats_covered__(self):
1720 objs = RunFilterStatistic.objects.select_related("runconfig")
1722 # We want to look for all runs created after either when the filter
1723 # got associated, since the creation of the first runcfg that contains
1724 # a failure that retro-actively associated to this issue. Pick the
1725 # earliest of these two events.
1726 start_time = self.added_on
1727 if self.covers_from < start_time:
1728 start_time = self.covers_from
1730 if self.deleted_on is not None:
1731 return objs.filter(runconfig__added_on__gte=start_time,
1732 runconfig__added_on__lt=self.deleted_on,
1733 covered_count__gt=0,
1734 filter__id=self.filter_id).order_by('-id')
1735 else:
1736 return objs.filter(runconfig__added_on__gte=start_time,
1737 covered_count__gt=0,
1738 filter__id=self.filter_id).order_by('-id')
1740 @cached_property
1741 def runconfigs_covered(self):
1742 return set([r.runconfig for r in self.__runfilter_stats_covered__])
1744 @cached_property
1745 def runconfigs_affected(self):
1746 return set([r.runconfig for r in self.__runfilter_stats_covered__ if r.matched_count > 0])
1748 @property
1749 def covered_results(self):
1750 q = self.filter.covered_results.filter(ts_run__runconfig__added_on__gte=self.covers_from)
1751 return q.prefetch_related('ts_run', 'ts_run__runconfig')
1753 def _add_missing_stats(self):
1754 # Find the list of runconfig we have stats for
1755 runconfigs_done = RunFilterStatistic.objects.filter(filter=self.filter).values_list('runconfig', flat=True)
1757 # Get the list of results, excluding the ones coming from runconfigs we already have
1758 stats = dict()
1759 results = (
1760 self.covered_results.exclude(ts_run__runconfig__id__in=runconfigs_done)
1761 .filter(ts_run__runconfig__temporary=False)
1762 .only("id", "ts_run")
1763 )
1764 for result in results:
1765 runconfig = result.ts_run.runconfig
1766 fs = stats.get(runconfig)
1767 if fs is None:
1768 stats[runconfig] = fs = RunFilterStatistic(filter=self.filter, runconfig=runconfig,
1769 matched_count=0, covered_count=0)
1771 fs.covered_count += 1
1773 # Now that we know which results are covered, we just need to refine our
1774 # query to also check if they matched.
1775 #
1776 # To avoid asking the database to re-do the coverage test, just use the
1777 # list of ids we got previously
1778 query = QueryParser(TestResult, self.filter._to_user_query(covers=False, matches=True)).objects
1779 query = query.filter(id__in=[r.id for r in results]).only('ts_run').prefetch_related('ts_run__runconfig')
1780 for result in query:
1781 stats[result.ts_run.runconfig].matched_count += 1
1783 # Save the statistics objects
1784 for fs in stats.values():
1785 fs.save()
1787 def update_statistics(self):
1788 # drop all the caches
1789 try:
1790 del self.__runfilter_stats_covered__
1791 del self.runconfigs_covered
1792 del self.runconfigs_affected
1793 except AttributeError:
1794 # Ignore the error if the cache had not been accessed before
1795 pass
1797 req = KnownFailure.objects.filter(matched_ifa=self, result__ts_run__runconfig__temporary=False)
1798 req = req.order_by("result__ts_run__runconfig__added_on")
1799 oldest_failure = req.values_list('result__ts_run__runconfig__added_on', flat=True).first()
1800 if oldest_failure is not None:
1801 self.covers_from = oldest_failure
1803 # get the list of runconfigs needing update
1804 self._add_missing_stats()
1806 self.runconfigs_covered_count = len(self.runconfigs_covered)
1807 self.runconfigs_affected_count = len(self.runconfigs_affected)
1809 # Find when the issue was last seen
1810 for stats in self.__runfilter_stats_covered__:
1811 if stats.matched_count > 0:
1812 self.last_seen = stats.runconfig.added_on
1813 self.last_seen_runconfig = stats.runconfig
1814 break
1816 # Update the statistics atomically in the DB
1817 cur_ifa = IssueFilterAssociated.objects.filter(id=self.id)
1818 cur_ifa.update(covers_from=self.covers_from,
1819 runconfigs_covered_count=self.runconfigs_covered_count,
1820 runconfigs_affected_count=self.runconfigs_affected_count,
1821 last_seen=self.last_seen,
1822 last_seen_runconfig=self.last_seen_runconfig)
1824 @property
1825 def failure_rate(self):
1826 return Rate("runs", self.runconfigs_affected_count, self.runconfigs_covered_count)
1828 @property
1829 def activity_period(self):
1830 added_by = " by {}".format(render_to_string("CIResults/basic/user.html",
1831 {"user": self.added_by}).strip()) if self.added_by else ""
1832 deleted_by = " by {}".format(render_to_string("CIResults/basic/user.html",
1833 {"user": self.deleted_by}).strip()) if self.deleted_by else ""
1835 if not self.active:
1836 s = "Added {}{}, removed {}{} (was active for {})"
1837 return s.format(naturaltime(self.added_on), added_by,
1838 naturaltime(self.deleted_on), deleted_by,
1839 timesince(self.added_on, self.deleted_on))
1840 else:
1841 return "Added {}{}".format(naturaltime(self.added_on), added_by)
1843 def __str__(self):
1844 if self.deleted_on is not None:
1845 delete_on = " - deleted on {}".format(self.deleted_on)
1846 else:
1847 delete_on = ""
1849 return "{} on {}{}".format(self.filter.description, self.issue, delete_on)
1852class Issue(models.Model, UserFiltrableMixin):
1853 filter_objects_to_db = {
1854 'filter_description': FilterObjectStr('filters__description',
1855 'Description of what the filter matches'),
1856 'filter_runconfig_tag_name': FilterObjectStr('filters__tags__name',
1857 'Run configuration tag matched by the filter'),
1858 'filter_machine_tag_name': FilterObjectStr('filters__machine_tags__name',
1859 'Machine tag matched by the filter'),
1860 'filter_machine_name': FilterObjectStr('filters__machines__name',
1861 'Name of a machine matched by the filter'),
1862 'filter_test_name': FilterObjectStr('filters__tests__name',
1863 'Name of a test matched by the filter'),
1864 'filter_status_name': FilterObjectStr('filters__statuses__name',
1865 'Status matched by the filter'),
1866 'filter_stdout_regex': FilterObjectStr('filters__stdout_regex',
1867 'Regular expression for the standard output used by the filter'),
1868 'filter_stderr_regex': FilterObjectStr('filters__stderr_regex',
1869 'Regular expression for the error output used by the filter'),
1870 'filter_dmesg_regex': FilterObjectStr('filters__dmesg_regex',
1871 'Regular expression for the kernel logs used by the filter'),
1872 'filter_added_on': FilterObjectDateTime('issuefilterassociated__added_on',
1873 'Date at which the filter was associated to the issue'),
1874 'filter_covers_from': FilterObjectDateTime('issuefilterassociated__covers_from',
1875 'Date of the first failure covered by the filter'),
1876 'filter_deleted_on': FilterObjectDateTime('issuefilterassociated__deleted_on',
1877 'Date at which the filter was deleted from the issue'),
1878 'filter_runconfigs_covered_count': FilterObjectInteger('issuefilterassociated__runconfigs_covered_count',
1879 'Amount of run configurations covered by the filter'),
1880 'filter_runconfigs_affected_count': FilterObjectInteger('issuefilterassociated__runconfigs_affected_count',
1881 'Amount of run configurations affected by the filter'),
1882 'filter_last_seen': FilterObjectDateTime('issuefilterassociated__last_seen',
1883 'Date at which the filter last matched'),
1884 'filter_last_seen_runconfig_name': FilterObjectStr('issuefilterassociated__last_seen_runconfig__name',
1885 'Run configuration which last matched the filter'),
1887 'bug_tracker_name': FilterObjectStr('bugs__tracker__name',
1888 'Name of the tracker hosting the bug associated to the issue'),
1889 'bug_tracker_short_name': FilterObjectStr('bugs__tracker__short_name',
1890 'Short name of the tracker hosting the bug associated to the issue'),
1891 'bug_tracker_type': FilterObjectStr('bugs__tracker__tracker_type',
1892 'Type of tracker hosting the bug associated to the issue'),
1893 'bug_id': FilterObjectStr('bugs__bug_id',
1894 'ID of the bug associated to the issue'),
1895 'bug_title': FilterObjectStr('bugs__title',
1896 'Title of the bug associated to the issue'),
1897 'bug_created_on': FilterObjectDateTime('bugs__created',
1898 'Date at which the bug associated to the issue was created'),
1899 'bug_updated_on': FilterObjectDateTime('bugs__updated',
1900 'Date at which the bug associated to the issue was last updated'),
1901 'bug_closed_on': FilterObjectDateTime('bugs__closed',
1902 'Date at which the bug associated to the issue was closed'),
1903 'bug_creator_name': FilterObjectStr('bugs__creator__person__full_name',
1904 'Name of the creator of the bug associated to the issue'),
1905 'bug_creator_email': FilterObjectStr('bugs__creator__person__email',
1906 'Email address of the creator of the bug associated to the issue'),
1907 'bug_assignee_name': FilterObjectStr('bugs__assignee__person__full_name',
1908 'Name of the assignee of the bug associated to the issue'),
1909 'bug_assignee_email': FilterObjectStr('bugs__assignee__person__email',
1910 'Email address of the assignee of the bug associated to the issue'),
1911 'bug_product': FilterObjectStr('bugs__product', 'Product of the bug associated to the issue'),
1912 'bug_component': FilterObjectStr('bugs__component', 'Component of the bug associated to the issue'),
1913 'bug_priority': FilterObjectStr('bugs__priority', 'Priority of the bug associated to the issue'),
1914 'bug_features': FilterObjectStr('bugs__features',
1915 'Features of the bug associated to the issue (coma-separated list)'),
1916 'bug_platforms': FilterObjectStr('bugs__platforms',
1917 'Platforms of the bug associated to the issue (coma-separated list)'),
1918 'bug_status': FilterObjectStr('bugs__status',
1919 'Status of the bug associated to the issue'),
1920 'bug_severity': FilterObjectStr('bugs__severity', 'Severity of the bug associated to the issue'),
1921 'bug_tags': FilterObjectStr('bugs__tags',
1922 'Tags/labels on the bug associated to this issue (coma-separated list)'),
1924 'description': FilterObjectStr('description', 'Free-hand text associated to the issue by the bug filer'),
1925 'filer_email': FilterObjectStr('filer', 'Email address of the person who filed the issue (DEPRECATED)'),
1927 'id': FilterObjectInteger('id', 'Id of the issue'),
1929 'added_on': FilterObjectDateTime('added_on', 'Date at which the issue was created'),
1930 'added_by': FilterObjectStr('added_by__username', 'Username of the person who filed the issue'),
1931 'archived_on': FilterObjectDateTime('archived_on', 'Date at which the issue was archived'),
1932 'archived_by': FilterObjectStr('archived_by__username', 'Username of the person who archived the issue'),
1933 'expected': FilterObjectBool('expected', 'Is the issue expected?'),
1934 'runconfigs_covered_count': FilterObjectInteger('runconfigs_covered_count',
1935 'Amount of run configurations covered by the issue'),
1936 'runconfigs_affected_count': FilterObjectInteger('runconfigs_affected_count',
1937 'Amount of run configurations affected by the issue'),
1938 'last_seen': FilterObjectDateTime('last_seen', 'Date at which the issue was last seen'),
1939 'last_seen_runconfig_name': FilterObjectStr('last_seen_runconfig__name',
1940 'Run configuration which last reproduced the issue'),
1941 }
1943 filters = models.ManyToManyField(IssueFilter, through="IssueFilterAssociated")
1944 bugs = models.ManyToManyField(Bug)
1946 description = models.TextField(blank=True)
1947 filer = models.EmailField() # DEPRECATED
1949 added_on = models.DateTimeField(auto_now_add=True)
1950 added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='issue_creator',
1951 null=True, on_delete=models.SET(get_sentinel_user))
1953 archived_on = models.DateTimeField(blank=True, null=True, db_index=True)
1954 archived_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='issue_archiver',
1955 null=True, on_delete=models.SET(get_sentinel_user))
1957 expected = models.BooleanField(default=False, db_index=True,
1958 help_text="Is this issue expected and should be considered an active issue?")
1960 # Statistics cache
1961 runconfigs_covered_count = models.PositiveIntegerField(default=0)
1962 runconfigs_affected_count = models.PositiveIntegerField(default=0)
1963 last_seen = models.DateTimeField(null=True, blank=True)
1964 last_seen_runconfig = models.ForeignKey('RunConfig', null=True, blank=True, on_delete=models.SET_NULL)
1966 class Meta:
1967 permissions = [
1968 ("archive_issue", "Can archive issues"),
1969 ("restore_issue", "Can restore issues"),
1971 ("hide_issue", "Can hide / mark as un-expected"),
1972 ("show_issue", "Can show / mark as as expected"),
1973 ]
1975 @property
1976 def archived(self):
1977 return self.archived_on is not None
1979 def hide(self):
1980 self.expected = True
1981 self.save()
1983 def show(self):
1984 self.expected = False
1985 self.save()
1987 @cached_property
1988 def active_filters(self):
1989 if self.archived:
1990 deleted_on = self.archived_on
1991 else:
1992 deleted_on = None
1994 if hasattr(self, 'ifas_cached'):
1995 ifas = filter(lambda i: i.deleted_on == deleted_on, self.ifas_cached)
1996 return sorted(ifas, reverse=True, key=lambda i: i.id)
1997 else:
1998 ifas = IssueFilterAssociated.objects.filter(issue=self,
1999 deleted_on=deleted_on)
2000 return ifas.select_related("filter").order_by('-id')
2002 @cached_property
2003 def all_filters(self):
2004 return IssueFilterAssociated.objects.filter(issue=self).select_related('filter').order_by('id')
2006 @property
2007 def past_filters(self):
2008 return [ifa for ifa in self.all_filters if ifa.deleted_on != self.archived_on]
2010 @cached_property
2011 def bugs_cached(self):
2012 # HACK: Sort by decreasing ID in python so as we can prefetch the bugs
2013 # in the main view, saving as many SQL requests as we have bugs
2014 return sorted(self.bugs.all(), reverse=True, key=lambda b: b.id)
2016 @cached_property
2017 def covers_from(self):
2018 return min([self.added_on] + [min(ifa.added_on, ifa.covers_from) for ifa in self.all_filters])
2020 @cached_property
2021 def __runfilter_stats_covered__(self):
2022 filters = [e.filter for e in self.all_filters]
2023 objs = RunFilterStatistic.objects.select_related("runconfig")
2024 objs = objs.filter(runconfig__added_on__gte=self.covers_from,
2025 covered_count__gt=0,
2026 filter__in=filters).order_by("-runconfig__added_on")
2027 if self.archived:
2028 objs = objs.filter(runconfig__added_on__lt=self.archived_on)
2030 return objs
2032 @cached_property
2033 def runconfigs_covered(self):
2034 return set([r.runconfig for r in self.__runfilter_stats_covered__])
2036 @cached_property
2037 def runconfigs_affected(self):
2038 # Go through all the RunFilterStats covered by this issue and add runs
2039 # to the set of affected ones
2040 runconfigs_affected = set()
2041 for runfilter in self.__runfilter_stats_covered__:
2042 if runfilter.matched_count > 0:
2043 runconfigs_affected.add(runfilter.runconfig)
2045 return runconfigs_affected
2047 def update_statistics(self):
2048 self.runconfigs_covered_count = len(self.runconfigs_covered)
2049 self.runconfigs_affected_count = len(self.runconfigs_affected)
2051 # Find when the issue was last seen
2052 for stats in self.__runfilter_stats_covered__:
2053 if stats.matched_count > 0:
2054 self.last_seen = stats.runconfig.added_on
2055 self.last_seen_runconfig = stats.runconfig
2056 break
2058 # Update the statistics atomically in the DB
2059 cur_issue = Issue.objects.filter(id=self.id)
2060 cur_issue.update(runconfigs_covered_count=self.runconfigs_covered_count,
2061 runconfigs_affected_count=self.runconfigs_affected_count,
2062 last_seen=self.last_seen,
2063 last_seen_runconfig=self.last_seen_runconfig)
2065 @property
2066 def failure_rate(self):
2067 return Rate("runs", self.runconfigs_affected_count, self.runconfigs_covered_count)
2069 def matches(self, result):
2070 if self.archived:
2071 return False
2073 for e in IssueFilterAssociated.objects_ready_for_matching.filter(deleted_on=None):
2074 if e.filter.matches(result):
2075 return True
2076 return False
2078 def archive(self, user):
2079 if self.archived:
2080 raise ValueError("The issue is already archived")
2082 with transaction.atomic():
2083 now = timezone.now()
2084 for e in IssueFilterAssociated.objects.filter(issue=self):
2085 e.delete(user, now)
2086 self.archived_on = now
2087 self.archived_by = user
2088 self.save()
2090 # Post a comment
2091 self.render_and_leave_comment_on_all_bugs("CIResults/issue_archived.txt", issue=self)
2093 def restore(self):
2094 if not self.archived:
2095 raise ValueError("The issue is not currently archived")
2097 # re-add all the filters that used to be associated
2098 with transaction.atomic():
2099 for e in IssueFilterAssociated.objects.filter(issue=self, deleted_on=self.archived_on):
2100 self.__filter_add__(e.filter, e.added_by)
2102 # Mark the issue as not archived anymore before saving the changes
2103 self.archived_on = None
2104 self.archived_by = None
2105 self.save()
2107 # Now update our statistics since we possibly re-assigned some new failures
2108 self.update_statistics()
2110 # Post a comment
2111 self.render_and_leave_comment_on_all_bugs("CIResults/issue_restored.txt", issue=self)
2113 @transaction.atomic
2114 def set_bugs(self, bugs):
2115 if self.archived:
2116 raise ValueError("The issue is archived, and thus read-only")
2118 # Let's simply delete all the bugs before adding them back
2119 self.bugs.clear()
2121 for bug in bugs:
2122 # Add first runconfig the bug was seen in
2123 bug.add_first_seen_in(self)
2125 # Make sure the bug exists in the database
2126 if bug.id is None:
2127 bug.save()
2129 # Add it to the relation
2130 self.bugs.add(bug)
2132 # Get rid of the cached bugs
2133 try:
2134 del self.bugs_cached
2135 except AttributeError:
2136 # Ignore the error if the cached had not been accessed before
2137 pass
2139 def _assign_to_known_failures(self, unknown_failures, ifa):
2140 now = timezone.now()
2141 new_matched_failures = []
2142 for failure in unknown_failures:
2143 filing_delay = now - failure.result.ts_run.reported_on
2144 kf = KnownFailure.objects.create(result=failure.result, matched_ifa=ifa,
2145 manually_associated_on=now,
2146 filing_delay=filing_delay)
2147 new_matched_failures.append(kf)
2148 failure.delete()
2150 ifa.update_statistics()
2152 return new_matched_failures
2154 def __filter_add__(self, filter, user):
2155 # Make sure the filter exists in the database first
2156 if filter.id is None:
2157 filter.save()
2159 # Create the association between the filter and the issue
2160 ifa = IssueFilterAssociated.objects.create(filter=filter, issue=self, added_by=user)
2162 # Go through the untracked issues and check if the filter matches any of
2163 # them. Also include the unknown failures from temporary runs.
2164 matched_unknown_failures = (
2165 filter.matched_unknown_failures.select_related("result")
2166 .prefetch_related("result__ts_run")
2167 .defer("result__stdout", "result__stderr", "result__dmesg")
2168 )
2169 return self._assign_to_known_failures(matched_unknown_failures, ifa)
2171 def render_and_leave_comment_on_all_bugs(self, template: str, is_new: bool = False, /, **template_kwargs) -> None:
2172 basic_comment: str = render_to_string(template, template_kwargs)
2173 basic_comment += "" # Add an empty string to get a string instead of safetext
2175 try:
2176 for bug in self.bugs_cached:
2177 if is_new and bug.first_seen_in:
2178 template_kwargs["first_seen_in"] = bug.first_seen_in.name
2179 comment: str = render_to_string(template, template_kwargs)
2180 comment += "" # Add an empty string to get a string instead of safetext
2181 else:
2182 comment = basic_comment
2183 bug.add_comment(comment)
2184 except Exception: # pragma: no cover
2185 traceback.print_exc() # pragma: no cover
2187 def replace_filter(self, old_filter, new_filter, user):
2188 if self.archived:
2189 raise ValueError("The issue is archived, and thus read-only")
2191 with transaction.atomic():
2192 # First, add the new filter
2193 failures = self.__filter_add__(new_filter, user)
2194 new_matched_failures = [f for f in failures if not f.result.ts_run.runconfig.temporary]
2196 # Delete all active associations of the old filter
2197 assocs = IssueFilterAssociated.objects.filter(deleted_on=None, filter=old_filter)
2198 for e in assocs:
2199 e.delete(user, timezone.now())
2201 # Now update our statistics since we possibly re-assigned some new failures
2202 self.update_statistics()
2204 # Post a comment on the bugs associated to this issue if something changed
2205 if (old_filter.description != new_filter.description or
2206 old_filter.equivalent_user_query != new_filter.equivalent_user_query or
2207 len(new_matched_failures) > 0):
2208 self.render_and_leave_comment_on_all_bugs(
2209 "CIResults/issue_replace_filter_comment.txt",
2210 issue=self,
2211 old_filter=old_filter,
2212 new_filter=new_filter,
2213 new_matched_failures=new_matched_failures,
2214 user=user,
2215 )
2217 def set_filters(self, filters, user):
2218 if self.archived:
2219 raise ValueError("The issue is archived, and thus read-only")
2221 with transaction.atomic():
2222 removed_ifas = set()
2223 new_filters = dict()
2225 # Query the set of issues that we currently have
2226 assocs = IssueFilterAssociated.objects.filter(deleted_on=None, issue=self)
2228 # First, "delete" all the filters that are not in the new set
2229 now = timezone.now()
2230 for e in assocs:
2231 if e.filter not in filters:
2232 e.delete(user, now)
2233 removed_ifas.add(e)
2235 # Now, let's add all the new ones
2236 cur_filters_ids = set([e.filter.id for e in assocs])
2237 for filter in filters:
2238 if filter.id not in cur_filters_ids:
2239 new_filters[filter] = self.__filter_add__(filter, user)
2241 # Now update our statistics since we possibly re-assigned some new failures
2242 self.update_statistics()
2244 # Get rid of the cached filters
2245 try:
2246 del self.active_filters
2247 except AttributeError:
2248 # Ignore the error if the cache had not been accessed before
2249 pass
2251 # Post a comment on the bugs associated to this issue
2252 if len(removed_ifas) > 0 or len(new_filters) > 0:
2253 self.render_and_leave_comment_on_all_bugs(
2254 "CIResults/issue_set_filters_comment.txt",
2255 True,
2256 issue=self,
2257 removed_ifas=removed_ifas,
2258 new_filters=new_filters,
2259 user=user,
2260 )
2262 @transaction.atomic
2263 def merge_issues(self, issues, user):
2264 # TODO: This is just a definition of interface, the code is untested
2266 # First, add all our current filters to a list
2267 new_issue_filters = [filter for filter in self.filters.all()]
2269 # Collect the list of filters from the issues we want to merge before
2270 # archiving them
2271 for issue in issues:
2272 for filter in issue.filters.all():
2273 new_issue_filters.append(filter)
2274 issue.archive(user)
2276 # Set the new list of filters
2277 self.set_filters(new_issue_filters, user)
2279 # Now update our statistics since we possibly re-assigned some new failures
2280 self.update_statistics()
2282 def __str__(self):
2283 bugs = self.bugs.all()
2284 if len(bugs) == 0:
2285 return "Issue: <empty>"
2286 elif len(bugs) == 1:
2287 return "Issue: " + str(bugs[0])
2288 else:
2289 return "Issue: [{}]".format(", ".join([b.short_name for b in bugs]))
2292class KnownFailure(models.Model, UserFiltrableMixin):
2293 # For the FilterMixin.
2294 filter_objects_to_db = {
2295 'runconfig': FilterObjectModel(RunConfig, 'result__ts_run__runconfig', 'Run configuration the test is part of'),
2296 'runconfig_name': FilterObjectStr('result__ts_run__runconfig__name', 'Name of the run configuration'),
2297 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name',
2298 'Tag associated with the configuration used for this test execution'),
2299 'runconfig_added_on': FilterObjectDateTime('result__ts_run__runconfig__added_on',
2300 'Date at which the run configuration got created'),
2301 'runconfig_temporary': FilterObjectBool('result__ts_run__runconfig__temporary',
2302 'Is the run configuration temporary, like for pre-merge testing?'),
2303 'build_name': FilterObjectStr('result__ts_run__runconfig__builds__name',
2304 'Name of the build for a component used for this test execution'),
2305 'build_added_on': FilterObjectDateTime('result__ts_run__runconfig__builds__added_on',
2306 'Date at which the build was added'),
2307 'component_name': FilterObjectStr('result__ts_run__runconfig__builds__component__name',
2308 'Name of a component used for this test execution'),
2309 'machine_name': FilterObjectStr('result__ts_run__machine__name', 'Name of the machine used for this result'),
2310 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name',
2311 'Tag associated to the machine used in this run'),
2312 'status_name': FilterObjectStr('result__status__name', 'Name of the resulting status (pass/fail/crash/...)'),
2313 'testsuite_name': FilterObjectStr('result__status__testsuite__name',
2314 'Name of the testsuite that contains this test'),
2315 'test_name': FilterObjectStr('result__test__name', 'Name of the test'),
2316 'test_added_on': FilterObjectDateTime('result__test__added_on', 'Date at which the test got added'),
2317 'manually_filed_on': FilterObjectDateTime('manually_associated_on',
2318 'Date at which the failure got manually associated to an issue'),
2319 'ifa_id': FilterObjectInteger('matched_ifa_id',
2320 'ID of the associated filter that matched the failure'),
2321 'issue_id': FilterObjectInteger('matched_ifa__issue_id',
2322 'ID of the issue associated to the failure'),
2323 'issue_expected': FilterObjectBool('matched_ifa__issue__expected',
2324 'Is the issue associated to the failure marked as expected?'),
2325 'filter_description': FilterObjectStr('matched_ifa__issue__filters__description',
2326 'Description of what the filter associated to the failure'),
2327 'filter_runconfig_tag_name':
2328 FilterObjectStr('matched_ifa__issue__filters__tags__name',
2329 'Run configuration tag matched by the filter associated to the failure'),
2330 'filter_machine_tag_name': FilterObjectStr('matched_ifa__issue__filters__machine_tags__name',
2331 'Machine tag matched by the filter associated to the failure'),
2332 'filter_machine_name': FilterObjectStr('matched_ifa__issue__filters__machines__name',
2333 'Name of a machine matched by the filter associated to the failure'),
2334 'filter_test_name': FilterObjectStr('matched_ifa__issue__filters__tests__name',
2335 'Name of a test matched by the filter associated to the failure'),
2336 'filter_status_name': FilterObjectStr('matched_ifa__issue__filters__statuses__name',
2337 'Status matched by the filter associated to the failure'),
2338 'filter_stdout_regex': FilterObjectStr('matched_ifa__issue__filters__stdout_regex',
2339 'Standard output regex used by the filter associated to the failure'),
2340 'filter_stderr_regex': FilterObjectStr('matched_ifa__issue__filters__stderr_regex',
2341 'Standard error regex used by the filter associated to the failure'),
2342 'filter_dmesg_regex': FilterObjectStr('matched_ifa__issue__filters__dmesg_regex',
2343 'Regex for dmesg used by the filter associated to the failure'),
2344 'filter_added_on': FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__added_on',
2345 'Date at which the filter associated to the failure was added on to its issue'), # noqa
2346 'filter_covers_from':
2347 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__covers_from',
2348 'Date of the first failure covered by the filter associated to the failure'),
2349 'filter_deleted_on':
2350 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__deleted_on',
2351 'Date at which the filter was removed from the issue associated to the failure'),
2352 'filter_runconfigs_covered_count':
2353 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_covered_count',
2354 'Amount of run configurations covered by the filter associated to the failure'),
2355 'filter_runconfigs_affected_count':
2356 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_affected_count',
2357 'Amount of run configurations affected by the filter associated to the failure'),
2358 'filter_last_seen':
2359 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__last_seen',
2360 'Date at which the filter matching this failure was last seen'),
2361 'filter_last_seen_runconfig_name':
2362 FilterObjectStr('matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name',
2363 'Run configuration which last matched the filter associated to the failure'),
2364 'bug_tracker_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__name',
2365 'Name of the tracker which holds the bug associated to this failure'),
2366 'bug_tracker_short_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__short_name',
2367 'Short name of the tracker which holds the bug associated to this failure'), # noqa
2368 'bug_tracker_type': FilterObjectStr('matched_ifa__issue__bugs__tracker__tracker_type',
2369 'Type of the tracker which holds the bug associated to this failure'),
2370 'bug_id': FilterObjectStr('matched_ifa__issue__bugs__bug_id',
2371 'ID of the bug associated to this failure'),
2372 'bug_title': FilterObjectStr('matched_ifa__issue__bugs__title',
2373 'Title of the bug associated to this failure'),
2374 'bug_created_on': FilterObjectDateTime('matched_ifa__issue__bugs__created',
2375 'Date at which the bug associated to this failure was created'),
2376 'bug_closed_on': FilterObjectDateTime('matched_ifa__issue__bugs__closed',
2377 'Date at which the bug associated to this failure was closed'),
2378 'bug_creator_name': FilterObjectStr('matched_ifa__issue__bugs__creator__person__full_name',
2379 'Name of the creator of the bug associated to this failure'),
2380 'bug_creator_email': FilterObjectStr('matched_ifa__issue__bugs__creator__person__email',
2381 'Email address of the creator of the bug associated to this failure'),
2382 'bug_assignee_name': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__full_name',
2383 'Name of the assignee of the bug associated to this failure'),
2384 'bug_assignee_email': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__email',
2385 'Email address of the assignee of the bug associated to this failure'),
2386 'bug_product': FilterObjectStr('matched_ifa__issue__bugs__product',
2387 'Product of the bug associated to this failure'),
2388 'bug_component': FilterObjectStr('matched_ifa__issue__bugs__component',
2389 'Component of the bug associated to this failure'),
2390 'bug_priority': FilterObjectStr('matched_ifa__issue__bugs__priority',
2391 'Priority of the bug associated to this failure'),
2392 'bug_features': FilterObjectStr('matched_ifa__issue__bugs__features',
2393 'Features of the bug associated to this failure (coma-separated list)'),
2394 'bug_platforms': FilterObjectStr('matched_ifa__issue__bugs__platforms',
2395 'Platforms of the bug associated to this failure (coma-separated list)'),
2396 'bug_status': FilterObjectStr('matched_ifa__issue__bugs__status',
2397 'Status of the bug associated to this failure'),
2398 'bug_tags': FilterObjectStr('matched_ifa__issue__bugs__tags',
2399 'Tags/labels on the bug associated to this failure (coma-separated list)'),
2400 'url': FilterObjectStr('result__url', 'External URL of this test result'),
2401 'start': FilterObjectDateTime('result__start', 'Date at which this test started being executed'),
2402 'duration': FilterObjectDuration('result__duration', 'Time it took to execute the test'),
2403 'command': FilterObjectStr('result__command', 'Command used to execute the test'),
2404 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'),
2405 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'),
2406 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'),
2407 }
2409 result = models.ForeignKey(TestResult, on_delete=models.CASCADE,
2410 related_name="known_failures", related_query_name="known_failure")
2411 matched_ifa = models.ForeignKey(IssueFilterAssociated, on_delete=models.CASCADE)
2413 # When was the mapping done (useful for metrics)
2414 manually_associated_on = models.DateTimeField(null=True, blank=True, db_index=True)
2415 filing_delay = models.DurationField(null=True, blank=True)
2417 @classmethod
2418 def _runconfig_index(cls, covered_list, runconfig):
2419 try:
2420 covered = sorted(covered_list, key=lambda r: r.added_on, reverse=True)
2421 return covered.index(runconfig)
2422 except ValueError:
2423 return None
2425 @cached_property
2426 def covered_runconfigs_since_for_issue(self):
2427 return self._runconfig_index(self.matched_ifa.issue.runconfigs_covered,
2428 self.result.ts_run.runconfig)
2430 @cached_property
2431 def covered_runconfigs_since_for_filter(self):
2432 return self._runconfig_index(self.matched_ifa.runconfigs_covered,
2433 self.result.ts_run.runconfig)
2435 def __str__(self):
2436 return "{} associated on {}".format(str(self.result), self.manually_associated_on)
2439class UnknownFailure(models.Model, UserFiltrableMixin):
2440 filter_objects_to_db = {
2441 'test_name': FilterObjectStr('result__test__name', "Name of the test"),
2442 'status_name': FilterObjectStr('result__status__name', "Name of the status of failure"),
2443 'testsuite_name': FilterObjectStr('result__status__testsuite__name',
2444 "Name of the testsuite that contains this test"),
2445 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', "Name of the tag associated to machine"),
2446 'machine_name': FilterObjectStr('result__ts_run__machine__name', "Name of the associated machine"),
2447 'runconfig_name': FilterObjectStr('result__ts_run__runconfig__name', "Name of the associated runconfig"),
2448 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', "Tag associated to runconfig"),
2449 'bug_title': FilterObjectStr('matched_archived_ifas__issue__bugs__description',
2450 "Description of bug associated to failure"),
2451 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'),
2452 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'),
2453 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'),
2454 'build_name': FilterObjectStr('result__test__first_runconfig__builds__name', 'Name of the associated build'),
2455 }
2456 # We cannot have two UnknownFailure for the same result
2457 result = models.OneToOneField(TestResult, on_delete=models.CASCADE,
2458 related_name="unknown_failure")
2460 matched_archived_ifas = models.ManyToManyField(IssueFilterAssociated)
2462 @cached_property
2463 def matched_archived_ifas_cached(self):
2464 return self.matched_archived_ifas.all()
2466 @cached_property
2467 def matched_issues(self):
2468 issues = set()
2469 for e in self.matched_archived_ifas_cached:
2470 issues.add(e.issue)
2471 return issues
2473 def __str__(self):
2474 return str(self.result)
2477# Allows us to know if a filter covers/matches a runconfig or not
2478class RunFilterStatistic(models.Model):
2479 runconfig = models.ForeignKey(RunConfig, on_delete=models.CASCADE)
2480 filter = models.ForeignKey(IssueFilter, on_delete=models.CASCADE)
2482 covered_count = models.PositiveIntegerField()
2483 matched_count = models.PositiveIntegerField()
2485 class Meta:
2486 constraints = [
2487 UniqueConstraint(fields=('runconfig', 'filter'), name='unique_runconfig_filter')
2488 ]
2490 def __str__(self):
2491 if self.covered_count > 0:
2492 perc = self.matched_count * 100 / self.covered_count
2493 else:
2494 perc = 0
2495 return "{} on {}: match rate {}/{} ({:.2f}%)".format(self.filter,
2496 self.runconfig,
2497 self.matched_count,
2498 self.covered_count,
2499 perc)