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