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