Coverage for CIResults / models.py: 95%

1123 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-10 09:22 +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 

13 

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 

28 

29from collections import defaultdict, OrderedDict 

30 

31from datetime import timedelta 

32import hashlib 

33import traceback 

34import re 

35 

36 

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 

41 

42 

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 )]) 

51 

52 @cached_property 

53 def color(self): 

54 if self.color_hex is not None: 

55 return self.color_hex 

56 

57 # Generate a random color 

58 blake2 = hashlib.blake2b() 

59 blake2.update(self.name.encode()) 

60 return "#" + blake2.hexdigest()[-7:-1] 

61 

62 

63# Bug tracking 

64 

65 

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") 

71 

72 class Meta: 

73 constraints = [ 

74 UniqueConstraint(fields=('tracker', 'priority'), name='unique_tracker_priority'), 

75 ] 

76 verbose_name_plural = "Bug Tracker SLAs" 

77 

78 def __str__(self): 

79 return "{}: {} -> {}".format(str(self.tracker), self.priority, naturaltime(self.SLA)) 

80 

81 

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) 

85 

86 added_on = models.DateTimeField(auto_now_add=True) 

87 

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 

91 

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)" 

100 

101 

102class BugTrackerAccount(models.Model): 

103 tracker = models.ForeignKey("BugTracker", on_delete=models.CASCADE) 

104 person = models.ForeignKey(Person, on_delete=models.CASCADE) 

105 

106 user_id = models.CharField(max_length=254, help_text="User ID on the bugtracker") 

107 

108 is_developer = models.BooleanField(help_text="Is this a developer's account?") 

109 

110 class Meta: 

111 constraints = [ 

112 UniqueConstraint(fields=('tracker', 'user_id'), name='unique_tracker_user_id'), 

113 ] 

114 

115 def __str__(self): 

116 return str(self.person) 

117 

118 

119class BugTracker(models.Model): 

120 """ Represents a bug tracker such as Bugzilla or Jira """ 

121 

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?") 

132 

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)") 

136 

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=)") 

140 

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.") 

151 

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)") 

170 

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 

177 

178 @cached_property 

179 def tracker(self): 

180 from .bugtrackers import Untracked, Bugzilla, Jira, GitLab 

181 

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)) 

192 

193 def poll(self, bug, force_polling_comments=False): 

194 self.tracker.poll(bug, force_polling_comments) 

195 bug.polled = self.tracker_time 

196 

197 def poll_all(self, stop_event=None, bugs=None): 

198 if bugs is None: 

199 bugs = Bug.objects.filter(tracker=self) 

200 

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 

205 

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 

211 

212 bug.save() 

213 

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() 

218 

219 @property 

220 def tracker_time(self): 

221 return self.tracker._get_tracker_time() 

222 

223 def to_tracker_tz(self, dt): 

224 return self.tracker._to_tracker_tz(dt) 

225 

226 @property 

227 def open_statuses(self): 

228 return self.tracker.open_statuses 

229 

230 def is_bug_open(self, bug): 

231 return bug.status in self.open_statuses 

232 

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(',')] 

239 

240 @transaction.atomic 

241 def get_or_create_bugs(self, ids): 

242 new_bugs = set() 

243 

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)) 

248 

249 return set(known_bugs).union(new_bugs) 

250 

251 def __set_tracker_to_bugs__(self, bugs): 

252 for bug in bugs: 

253 bug.tracker = self 

254 return bugs 

255 

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)) 

259 

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)) 

264 

265 return self.__set_tracker_to_bugs__(open_bugs) 

266 

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) 

273 

274 return bugs_in_issues 

275 

276 def followed_bugs(self): 

277 return self.__set_tracker_to_bugs__(self.open_bugs() | self.bugs_in_issues()) 

278 

279 def updated_bugs(self): 

280 if not self.polled: 

281 return self.followed_bugs() 

282 

283 all_bug_ids = set(Bug.objects.filter(tracker=self).values_list('bug_id', flat=True)) 

284 

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 

290 

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) 

295 

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())) 

299 

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) 

302 

303 return self.__set_tracker_to_bugs__(upd_bugs) 

304 

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)) 

310 

311 unrep_bugs = self.get_or_create_bugs(unrep_bug_ids) 

312 

313 return self.__set_tracker_to_bugs__(unrep_bugs) 

314 

315 def clean(self): 

316 if self.custom_fields_map is None: 

317 return 

318 

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]) 

323 

324 def save(self, *args, **kwargs): 

325 self.clean() 

326 super().save(*args, **kwargs) 

327 

328 def __str__(self): 

329 return self.name 

330 

331 

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 

382 

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'), 

396 

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 } 

420 

421 UPDATE_PENDING_TIMEOUT = timedelta(minutes=45) 

422 

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)") 

430 

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") 

480 

481 # TODO: Metric on time between creation and first assignment. Metric on time between creation and component updated 

482 

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.") 

486 

487 flagged_as_update_pending_on = models.DateTimeField(null=True, blank=True, 

488 help_text="Date at which a developer indicated their " 

489 "willingness to update the bug") 

490 custom_fields = models.JSONField(help_text="Mapping of customfields and values for the Bug. This field will be " 

491 "automatically populated based on BugTracker.custom_fields_map field", 

492 default=dict) 

493 first_seen_in = models.ForeignKey("RunConfig", null=True, blank=False, on_delete=models.SET_NULL) 

494 

495 class Meta: 

496 constraints = [ 

497 UniqueConstraint(fields=('tracker', 'bug_id'), name='unique_tracker_bug_id'), 

498 UniqueConstraint(fields=('tracker', 'parent'), name='unique_tracker_parent'), 

499 ] 

500 

501 rd_only_fields = ['id', 'bug_id', 'tracker_id', 'tracker', 'parent_id', 'parent'] 

502 

503 @property 

504 def short_name(self): 

505 return "{}{}{}".format(self.tracker.short_name, self.tracker.separator, self.bug_id) 

506 

507 @property 

508 def url(self): 

509 return "{}{}".format(self.tracker.bug_base_url, self.bug_id) 

510 

511 @property 

512 def features_list(self): 

513 if self.features is not None and len(self.features) > 0: 

514 return [f.strip() for f in self.features.split(',')] 

515 else: 

516 return [] 

517 

518 @property 

519 def platforms_list(self): 

520 if self.platforms is not None and len(self.platforms) > 0: 

521 return [p.strip() for p in self.platforms.split(',')] 

522 else: 

523 return [] 

524 

525 @property 

526 def tags_list(self): 

527 if self.tags is not None and len(self.tags) > 0: 

528 return [t.strip() for t in self.tags.split(',')] 

529 else: 

530 return [] 

531 

532 @property 

533 def is_open(self): 

534 return self.tracker.is_bug_open(self) 

535 

536 @property 

537 def has_new_comments(self): 

538 return self.comments_polled is None or self.comments_polled < self.updated 

539 

540 @cached_property 

541 def comments_cached(self): 

542 return BugComment.objects.filter(bug=self).prefetch_related("account", "account__person") 

543 

544 @cached_property 

545 def involves(self): 

546 actors = defaultdict(lambda: 0) 

547 actors[self.creator] += 1 # NOTE: on bugzilla, we will double count the first post 

548 for comment in self.comments_cached: 

549 actors[comment.account] += 1 

550 

551 sorted_actors = OrderedDict() 

552 for account in sorted(actors.keys(), key=lambda k: actors[k], reverse=True): 

553 sorted_actors[account] = actors[account] 

554 

555 return sorted_actors 

556 

557 def __last_updated_by__(self, is_dev): 

558 last = None 

559 for comment in self.comments_cached: 

560 # TODO: make that if a developer wrote a new bug, he/she needs to be considered as a user 

561 if comment.account.is_developer == is_dev and (last is None or comment.created_on > last): 

562 last = comment.created_on 

563 return last 

564 

565 @cached_property 

566 def last_updated_by_user(self): 

567 return self.__last_updated_by__(False) 

568 

569 @cached_property 

570 def last_updated_by_developer(self): 

571 return self.__last_updated_by__(True) 

572 

573 @cached_property 

574 def SLA(self): 

575 if self.priority is not None: 

576 return self.tracker.SLAs_cached.get(self.priority.lower(), timedelta.max) 

577 else: 

578 return timedelta.max 

579 

580 @cached_property 

581 def SLA_deadline(self): 

582 if self.last_updated_by_developer is not None: 

583 # We have a comment, follow the SLA of the bug 

584 if self.SLA != timedelta.max: 

585 return self.last_updated_by_developer + self.SLA 

586 else: 

587 return timezone.now() + timedelta(days=365, seconds=1) 

588 else: 

589 # We have not done the initial triaging, give some time for the initial response 

590 return self.created + self.tracker.first_response_SLA 

591 

592 @cached_property 

593 def SLA_remaining_time(self): 

594 diff = self.SLA_deadline - timezone.now() 

595 return timedelta(seconds=int(diff.total_seconds())) 

596 

597 @cached_property 

598 def SLA_remaining_str(self): 

599 rt = self.SLA_remaining_time 

600 if rt < timedelta(0): 

601 return str(rt)[1:] + " ago" 

602 else: 

603 return "in " + str(rt) 

604 

605 @cached_property 

606 def effective_priority(self): 

607 return -self.SLA_remaining_time / self.SLA 

608 

609 @property 

610 def is_being_updated(self): 

611 if self.flagged_as_update_pending_on is None: 

612 return False 

613 else: 

614 return timezone.now() - self.flagged_as_update_pending_on < self.UPDATE_PENDING_TIMEOUT 

615 

616 @property 

617 def update_pending_expires_in(self): 

618 if self.flagged_as_update_pending_on is None: 

619 return None 

620 return (self.flagged_as_update_pending_on + self.UPDATE_PENDING_TIMEOUT) - timezone.now() 

621 

622 def clean(self): 

623 if self.custom_fields is None: 

624 return 

625 

626 for field, value in self.custom_fields.items(): 

627 if isinstance(value, dict) or isinstance(value, list) or isinstance(value, tuple): 

628 raise ValueError('Values stored in custom_fields cannot be tuples, lists, dictionaries') 

629 

630 def save(self, *args, **kwargs): 

631 self.clean() 

632 super().save(*args, **kwargs) 

633 

634 def update_from_dict(self, upd_dict): 

635 if not upd_dict: 

636 return 

637 

638 for field in upd_dict: 

639 # Disallow updating some critical fields 

640 if field in Bug.rd_only_fields: 

641 continue 

642 

643 if hasattr(self, field): 

644 setattr(self, field, upd_dict[field]) 

645 

646 def poll(self, force_polling_comments=False): 

647 self.tracker.poll(self, force_polling_comments) 

648 

649 def add_comment(self, comment): 

650 self.tracker.tracker.add_comment(self, comment) 

651 

652 def add_first_seen_in(self, issue: "Issue", save: bool) -> None: 

653 if self.first_seen_in is not None: 

654 return 

655 self.first_seen_in = ( 

656 RunConfig.objects 

657 .filter(testsuiterun__testresult__known_failure__matched_ifa__issue=issue) 

658 .order_by('added_on') 

659 .first() 

660 ) 

661 if save: 

662 self.save() 

663 

664 def create(self): 

665 try: 

666 id = self.tracker.tracker.create_bug(self) 

667 except ValueError: # pragma: no cover 

668 traceback.print_exc() # pragma: no cover 

669 else: 

670 self.bug_id = id 

671 

672 def __str__(self): 

673 return "{} - {}".format(self.short_name, self.title) 

674 

675 

676class BugComment(models.Model, UserFiltrableMixin): 

677 filter_objects_to_db = { 

678 'filter_description': 

679 FilterObjectStr('bug__issue__filters__description', 

680 'Description of what a filter associated to an issue referencing the bug on which the comment was made matches'), # noqa 

681 'filter_runconfig_tag_name': 

682 FilterObjectStr('bug__issue__filters__tags__name', 

683 'Run configuration tag matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

684 'filter_machine_tag_name': 

685 FilterObjectStr('bug__issue__filters__machine_tags__name', 

686 'Machine tag matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

687 'filter_machine_name': 

688 FilterObjectStr('bug__issue__filters__machines__name', 

689 'Name of a machine matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

690 'filter_test_name': 

691 FilterObjectStr('bug__issue__filters__tests__name', 

692 'Name of a test matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

693 'filter_status_name': 

694 FilterObjectStr('bug__issue__filters__statuses__name', 

695 'Status matched by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

696 'filter_stdout_regex': 

697 FilterObjectStr('bug__issue__filters__stdout_regex', 

698 'Standard output regex used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

699 'filter_stderr_regex': 

700 FilterObjectStr('bug__issue__filters__stderr_regex', 

701 'Standard error regex used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

702 'filter_dmesg_regex': 

703 FilterObjectStr('bug__issue__filters__dmesg_regex', 

704 'Regex for dmesg used by the filter associated to an issue referencing the bug on which the comment was made'), # noqa 

705 'filter_added_on': 

706 FilterObjectDateTime('bug__issue__issuefilterassociated__added_on', 

707 'Date at which the filter was associated to the issue referencing the bug on which the comment was made'), # noqa 

708 'filter_covers_from': 

709 FilterObjectDateTime('bug__issue__issuefilterassociated__covers_from', 

710 'Date of the first failure covered by the filter associated to the issue referencing the bug on which the comment was made'), # noqa 

711 'filter_deleted_on': 

712 FilterObjectDateTime('bug__issue__issuefilterassociated__deleted_on', 

713 'Date at which the filter was removed from the issue referencing the bug on which the comment was made'), # noqa 

714 'filter_runconfigs_covered_count': 

715 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_covered_count', 

716 'Amount of run configurations covered by the filter associated to the issue referencing the bug on which the comment was made'), # noqa 

717 'filter_runconfigs_affected_count': 

718 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_affected_count', 

719 'Amount of run configurations affected by the filter associated to the issue referencing the bug on which the comment was made'), # noqa 

720 'filter_last_seen': 

721 FilterObjectDateTime('bug__issue__issuefilterassociated__last_seen', 

722 'Date at which the filter associated to the issue referencing the bug on which the comment was made last matched'), # noqa 

723 'filter_last_seen_runconfig_name': 

724 FilterObjectStr('bug__issue__issuefilterassociated__last_seen_runconfig__name', 

725 'Run configuration which last matched the filter associated to the issue referencing the bug on which the comment was made'), # noqa 

726 

727 'issue_description': FilterObjectStr('bug__issue__description', 

728 'Free-hand text associated to the issue by the bug filer'), 

729 'issue_filer_email': FilterObjectStr('bug__issue__filer', 'Email address of the person who filed the issue'), 

730 'issue_added_on': FilterObjectDateTime('bug__issue__added_on', 'Date at which the issue was created'), 

731 'issue_archived_on': FilterObjectDateTime('bug__issue__archived_on', 'Date at which the issue was archived'), 

732 'issue_expected': FilterObjectBool('bug__issue__expected', 'Is the issue expected?'), 

733 'issue_runconfigs_covered_count': FilterObjectInteger('bug__issue__runconfigs_covered_count', 

734 'Amount of run configurations covered by the issue'), 

735 'issue_runconfigs_affected_count': FilterObjectInteger('bug__issue__runconfigs_affected_count', 

736 'Amount of run configurations affected by the issue'), 

737 'issue_last_seen': FilterObjectDateTime('bug__issue__last_seen', 'Date at which the issue was last seen'), 

738 'issue_last_seen_runconfig_name': FilterObjectStr('bug__issue__last_seen_runconfig__name', 

739 'Run configuration which last reproduced the issue'), 

740 

741 'tracker_name': FilterObjectStr('bug__tracker__name', 'Name of the tracker hosting the bug'), 

742 'tracker_short_name': FilterObjectStr('bug__tracker__short_name', 'Short name of the tracker which hosts the bug'), # noqa 

743 'tracker_type': FilterObjectStr('bug__tracker__tracker_type', 'Type of the tracker which hosts the bug'), 

744 'bug_id': FilterObjectStr('bug__bug_id', 'ID of the bug on which the comment was made'), 

745 'bug_title': FilterObjectStr('bug__title', 'Title of the bug on which the comment was made'), 

746 'bug_created_on': FilterObjectDateTime('bug__created', 

747 'Date at which the bug on which the comment was made was created'), 

748 'bug_updated_on': FilterObjectDateTime('bug__updated', 

749 'Date at which the bug on which the comment was made was last updated'), 

750 'bug_closed_on': FilterObjectDateTime('bug__closed', 

751 'Date at which the bug on which the comment was made was closed'), 

752 'bug_creator_name': FilterObjectStr('bug__creator__person__full_name', 

753 'Name of the creator of the bug on which the comment was made'), 

754 'bug_creator_email': FilterObjectStr('bug__creator__person__email', 

755 'Email address of the creator of the bug on which the comment was made'), 

756 'bug_assignee_name': FilterObjectStr('bug__assignee__person__full_name', 

757 'Name of the assignee of the bug on which the comment was made'), 

758 'bug_assignee_email': FilterObjectStr('bug__assignee__person__email', 

759 'Email address of the assignee of the bug on which the comment was made'), 

760 'bug_product': FilterObjectStr('bug__product', 'Product of the bug on which the comment was made'), 

761 'bug_component': FilterObjectStr('bug__component', 'Component of the bug on which the comment was made'), 

762 'bug_priority': FilterObjectStr('bug__priority', 'Priority of the bug on which the comment was made'), 

763 'bug_features': FilterObjectStr('bug__features', 

764 'Features affected (coma-separated list) in the bug on which the comment was made'), # noqa 

765 'bug_platforms': FilterObjectStr('bug__platforms', 

766 'Platforms affected (coma-separated list) in the bug on which the comment was made'), # noqa 

767 'bug_status': FilterObjectStr('bug__status', 

768 'Status of the bug (RESOLVED/FIXED, ...) on which the comment was made'), 

769 'bug_tags': FilterObjectStr('bug__tags', 'Tags/labels associated to the bug (coma-separated list)'), 

770 

771 'creator_name': FilterObjectStr('account__person__full_name', 'Name of the creator of the comment'), 

772 'creator_email': FilterObjectStr('account__person__email', 'Email address of the creator of the comment'), 

773 'creator_is_developer': FilterObjectBool('account__is_developer', 'Is the creator of the comment a developer?'), 

774 'comment_id': FilterObjectInteger('comment_id', 'The ID of the comment'), 

775 'created_on': FilterObjectDateTime('created_on', 'Date at wich the comment was made') 

776 } 

777 

778 bug = models.ForeignKey(Bug, on_delete=models.CASCADE) 

779 account = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE) 

780 

781 comment_id = models.CharField(max_length=20, help_text="The ID of the comment") 

782 url = models.URLField(null=True, blank=True) 

783 created_on = models.DateTimeField() 

784 

785 class Meta: 

786 constraints = [ 

787 UniqueConstraint(fields=('bug', 'comment_id'), name='unique_bug_comment_id'), 

788 ] 

789 

790 def __str__(self): 

791 return "{}'s comment by {}".format(self.bug, self.account) 

792 

793 

794def script_validator(script): 

795 try: 

796 client = Client.get_or_create_instance(script) 

797 except (ValueError, IOError) as e: 

798 raise ValidationError("Script contains syntax errors: {}".format(e)) 

799 else: 

800 client.shutdown() 

801 return script 

802 

803 

804class ReplicationScript(models.Model): 

805 """These scripts provide a method for replicating bugs between different bugtrackers, based 

806 on the user defined Python script. Further documentation on the process and API can be found 

807 here - :ref:`replication-doc` 

808 """ 

809 name = models.CharField(max_length=50, unique=True, help_text="Unique name for the script") 

810 created_by = models.ForeignKey(User, on_delete=models.CASCADE, help_text="The author or last editor of the script", 

811 related_name='script_creator', null=True, blank=True) 

812 created_on = models.DateTimeField(auto_now_add=True, 

813 help_text="Date the script was created or last updated") 

814 enabled = models.BooleanField(default=False, help_text="Enable bug replication") 

815 source_tracker = models.ForeignKey(BugTracker, related_name="source_rep_script", on_delete=models.CASCADE, 

816 null=True, help_text="Tracker to replicate from") 

817 destination_tracker = models.ForeignKey(BugTracker, related_name="dest_rep_script", on_delete=models.CASCADE, 

818 null=True, help_text="Tracker to replicate to") 

819 script = models.TextField(null=True, blank=True, 

820 help_text="Python script to be executed", validators=[script_validator]) 

821 script_history = models.TextField(default='[]', 

822 help_text="Stores the script edit history of the ReplicationScript model " 

823 "in JSON format. The keys correspond to all the fields in the ReplicationScript " 

824 "model, excluding this 'script_history' field itself.") 

825 

826 class Meta: 

827 constraints = [ 

828 UniqueConstraint( 

829 fields=('source_tracker', 'destination_tracker'), 

830 name='unique_source_tracker_destination_tracker', 

831 ), 

832 ] 

833 

834 def __str__(self): 

835 return "<replication script '{}'>".format(self.name) 

836 

837 

838# Software 

839class Component(models.Model): 

840 name = models.CharField(max_length=50, unique=True) 

841 description = models.TextField() 

842 url = models.URLField(null=True, blank=True) 

843 public = models.BooleanField(help_text="Should the component (and its builds) be visible on the public website?") 

844 

845 def __str__(self): 

846 return self.name 

847 

848 

849class Build(models.Model): 

850 # Minimum information needed 

851 name = models.CharField(max_length=60, unique=True) 

852 component = models.ForeignKey(Component, on_delete=models.CASCADE) 

853 version = models.CharField(max_length=40) 

854 added_on = models.DateTimeField(auto_now=True) 

855 

856 # Allow creating an overlay over the history of the component 

857 parents = models.ManyToManyField('Build', blank=True) 

858 

859 # Actual build information 

860 repo_type = models.CharField(max_length=50, null=True, blank=True) 

861 branch = models.CharField(max_length=50, null=True, blank=True) 

862 repo = models.CharField( 

863 max_length=200, 

864 null=True, 

865 blank=True, 

866 validators=[URLValidator(schemes=["ssh", "git", "git+ssh", "http", "https", "ftp", "ftps", "rsync", "file"])], 

867 ) 

868 upstream_url = models.URLField(null=True, blank=True) 

869 parameters = models.TextField(null=True, blank=True) 

870 build_log = models.TextField(null=True, blank=True) 

871 

872 @property 

873 def url(self): 

874 if self.upstream_url is not None: 

875 return self.upstream_url 

876 elif self.repo is not None: 

877 return "{} @ {}".format(self.version, self.repo) 

878 else: 

879 return self.version 

880 

881 def __str__(self): 

882 return self.name 

883 

884# Results 

885 

886 

887class VettableObjectMixin: 

888 @property 

889 def vetted(self): 

890 return self.vetted_on is not None 

891 

892 @transaction.atomic 

893 def vet(self): 

894 if self.vetted_on is not None: 

895 raise ValueError('The object is already vetted') 

896 self.vetted_on = timezone.now() 

897 self.save() 

898 

899 @transaction.atomic 

900 def suppress(self): 

901 if self.vetted_on is None: 

902 raise ValueError('The object is already suppressed') 

903 self.vetted_on = None 

904 self.save() 

905 

906 

907class Test(VettableObjectMixin, models.Model, UserFiltrableMixin): 

908 filter_objects_to_db = { 

909 'name': FilterObjectStr('name', "Name of the test"), 

910 'vetted_on': 

911 FilterObjectDateTime('vetted_on', "Datetime at which the test was vetted. None if the test is not vetted."), 

912 'added_on': FilterObjectDateTime('added_on', "Datetime at which the test was added"), 

913 'first_runconfig': 

914 FilterObjectStr('first_runconfig__name', "Name of the first non-temporary runconfig this test was seen in"), 

915 } 

916 name = models.CharField(max_length=150) 

917 testsuite = models.ForeignKey('TestSuite', on_delete=models.CASCADE) 

918 public = models.BooleanField(db_index=True, help_text="Should the test be visible on the public website?") 

919 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True, 

920 help_text="When did the test get ready for pre-merge testing?") 

921 added_on = models.DateTimeField(auto_now_add=True) 

922 first_runconfig = models.ForeignKey('RunConfig', db_index=True, null=True, on_delete=models.SET_NULL, 

923 help_text="First non-temporary runconfig that executed this test") 

924 

925 class Meta: 

926 ordering = ['name'] 

927 constraints = [ 

928 UniqueConstraint(fields=('name', 'testsuite'), name='unique_name_testsuite') 

929 ] 

930 permissions = [ 

931 ("vet_test", "Can vet a test"), 

932 ("suppress_test", "Can suppress a test"), 

933 ] 

934 

935 def __str__(self): 

936 return "{}: {}".format(self.testsuite, self.name) 

937 

938 @property 

939 def in_active_ifas(self): 

940 return IssueFilterAssociated.objects.filter(deleted_on=None, filter__tests__in=[self]) 

941 

942 @transaction.atomic 

943 def rename(self, new_name): 

944 # Get the matching test, or create it 

945 new_test = Test.objects.filter(name=new_name, testsuite=self.testsuite).first() 

946 if new_test is None: 

947 new_test = Test.objects.create(name=new_name, testsuite=self.testsuite, 

948 public=self.public) 

949 else: 

950 new_test.public = self.public 

951 

952 new_test.vetted_on = self.vetted_on 

953 new_test.save() 

954 

955 # Now, update every active IFA 

956 for ifa in self.in_active_ifas: 

957 ifa.filter.tests.add(new_test) 

958 

959 

960class MachineTag(models.Model): 

961 name = models.CharField(max_length=30, unique=True) 

962 description = models.TextField(help_text="Description of the objectives of the tag", blank=True, null=True) 

963 public = models.BooleanField(db_index=True, help_text="Should the machine tag be visible on the public website?") 

964 

965 added_on = models.DateTimeField(auto_now_add=True) 

966 

967 class Meta: 

968 ordering = ['name'] 

969 

970 @cached_property 

971 def machines(self): 

972 return sorted(Machine.objects.filter(tags__in=[self]), key=lambda m: m.name) 

973 

974 def __str__(self): 

975 return self.name 

976 

977 

978class Machine(VettableObjectMixin, ColoredObjectMixin, models.Model, UserFiltrableMixin): 

979 filter_objects_to_db = { 

980 'name': FilterObjectStr('name', "Name of the machine"), 

981 'description': FilterObjectStr('description', "Description of the machine"), 

982 'vetted_on': 

983 FilterObjectDateTime('vetted_on', 

984 "Datetime at which the machine was vetted. None if the machine is not vetted"), 

985 'added_on': FilterObjectDateTime('added_on', "Datetime at which the machine was added"), 

986 'aliases': FilterObjectStr('aliases__name', "Machine group this machine is a part of"), 

987 'tags': FilterObjectStr('tags__name', "List of tags associated to this machine"), 

988 } 

989 name = models.CharField(max_length=100, unique=True) 

990 description = models.TextField(help_text="Description of the machine", blank=True, null=True) 

991 

992 public = models.BooleanField(db_index=True, help_text="Should the machine be visible on the public website?") 

993 

994 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True, 

995 help_text="When did the machine get ready for pre-merge testing?") 

996 

997 added_on = models.DateTimeField(auto_now_add=True) 

998 

999 aliases = models.ForeignKey("Machine", on_delete=models.CASCADE, null=True, blank=True, 

1000 help_text="This machine is an alias of another machine. " 

1001 "The aliased machine will be used when comparing runconfigs. " 

1002 "This is useful if you have multiple identical machines that " 

1003 "execute a different subset of test every run") 

1004 

1005 tags = models.ManyToManyField(MachineTag, blank=True) 

1006 

1007 color_hex = ColoredObjectMixin.color_hex 

1008 

1009 class Meta: 

1010 ordering = ['name'] 

1011 permissions = [ 

1012 ("vet_machine", "Can vet a machine"), 

1013 ("suppress_machine", "Can suppress a machine"), 

1014 ] 

1015 

1016 @cached_property 

1017 def tags_cached(self): 

1018 return self.tags.all() 

1019 

1020 def __str__(self): 

1021 return self.name 

1022 

1023 

1024class RunConfigTag(models.Model): 

1025 name = models.CharField(max_length=50, unique=True, 

1026 help_text="Unique name for the tag") 

1027 description = models.TextField(help_text="Description of the objectives of the tag") 

1028 url = models.URLField(null=True, blank=True, help_text="URL to more explanations (optional)") 

1029 public = models.BooleanField(help_text="Should the tag be visible on the public website?") 

1030 

1031 def __str__(self): 

1032 return self.name 

1033 

1034 

1035class RunConfig(models.Model): 

1036 filter_objects_to_db = { 

1037 'name': FilterObjectStr('name', 'Name of the run configuration'), 

1038 'tag': FilterObjectStr('tags__name', 'Tag associated with the configuration for this run'), 

1039 'added_on': FilterObjectDateTime('added_on', 'Date at which the run configuration got created'), 

1040 'temporary': FilterObjectBool('temporary', 'Is the run configuration temporary (pre-merge testing)?'), 

1041 'build': FilterObjectStr('builds__name', 'Tag associated with the configuration for this run'), 

1042 'environment': FilterObjectStr('environment', 'Free-text field describing the environment of the machine'), 

1043 

1044 # Through reverse accessors 

1045 'machine_name': FilterObjectStr('testsuiterun__machine__name', 'Name of the machine used in this run'), 

1046 'machine_tag': FilterObjectStr('testsuiterun__machine__tags__name', 

1047 'Tag associated to the machine used in this run'), 

1048 } 

1049 

1050 name = models.CharField(max_length=70, unique=True) 

1051 tags = models.ManyToManyField(RunConfigTag) 

1052 temporary = models.BooleanField(help_text="This runconfig is temporary and should not be part of statistics") 

1053 url = models.URLField(null=True, blank=True) 

1054 

1055 added_on = models.DateTimeField(auto_now_add=True, db_index=True) 

1056 

1057 builds = models.ManyToManyField(Build) 

1058 

1059 environment = models.TextField(null=True, blank=True, 

1060 help_text="A human-readable, and machine-parsable definition of the environment. " 

1061 "Make sure the environment contains a header with the format and version.") 

1062 

1063 @cached_property 

1064 def tags_cached(self): 

1065 return self.tags.all() 

1066 

1067 @cached_property 

1068 def tags_ids_cached(self): 

1069 return set([t.id for t in self.tags_cached]) 

1070 

1071 @cached_property 

1072 def builds_cached(self): 

1073 return self.builds.all() 

1074 

1075 @cached_property 

1076 def builds_ids_cached(self): 

1077 return set([b.id for b in self.builds_cached]) 

1078 

1079 @cached_property 

1080 def public(self): 

1081 for tag in self.tags_cached: 

1082 if not tag.public: 

1083 return False 

1084 return True 

1085 

1086 @cached_property 

1087 def runcfg_history(self): 

1088 # TODO: we may want to use something else but the tags to find out 

1089 # the history of this particular run config 

1090 

1091 # TODO 2: make sure the tags sets are equal, not just that a set is inside 

1092 # another one. This is a regression caused by django 2.0 

1093 tags = self.tags_cached 

1094 return RunConfig.objects.order_by("-added_on").filter(tags__in=tags, temporary=False) 

1095 

1096 @cached_property 

1097 def runcfg_history_offset(self): 

1098 for i, runcfg in enumerate(self.runcfg_history): 

1099 if self.id == runcfg.id: 

1100 return i 

1101 raise ValueError("BUG: The runconfig ID has not been found in the runconfig history") 

1102 

1103 def __str__(self): 

1104 return self.name 

1105 

1106 def update_statistics(self): 

1107 stats = [] 

1108 

1109 # Do not compute statistics for temporary runconfigs 

1110 if self.temporary: 

1111 return stats 

1112 

1113 ifas = IssueFilterAssociated.objects_ready_for_matching.filter(Q(deleted_on=None)) 

1114 

1115 # Check if all filters cover and/or match results. De-dupplicate filters first 

1116 filters = set([e.filter for e in ifas]) 

1117 for filter in filters: 

1118 fs = RunFilterStatistic(filter=filter, runconfig=self, covered_count=0, 

1119 matched_count=0) 

1120 

1121 fs.covered_count = filter.covered_results.count() 

1122 if fs.covered_count < 1: 

1123 continue 

1124 matched_failures = [result for result in filter.matched_results if result.is_failure] 

1125 fs.matched_count = len(matched_failures) 

1126 stats.append(fs) 

1127 

1128 # Remove all the stats for the current run, and add the new ones 

1129 with transaction.atomic(): 

1130 RunFilterStatistic.objects.filter(runconfig=self, filter__in=filters).delete() 

1131 RunFilterStatistic.objects.bulk_create(stats) 

1132 

1133 return stats 

1134 

1135 def compare(self, to, max_missing_hosts=0.5, no_compress=False, query=None): 

1136 return RunConfigDiff(self, to, max_missing_hosts=max_missing_hosts, 

1137 no_compress=no_compress, query=query) 

1138 

1139 

1140class TestSuite(VettableObjectMixin, Component): 

1141 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True, 

1142 help_text="When did the testsuite get ready for pre-merge testing?") 

1143 

1144 # List of results you do not want to file bugs for 

1145 acceptable_statuses = models.ManyToManyField('TextStatus', related_name='+', blank=True) 

1146 

1147 # Status to ignore for diffing 

1148 notrun_status = models.ForeignKey('TextStatus', null=True, blank=True, on_delete=models.SET_NULL, 

1149 related_name='+') 

1150 

1151 class Meta: 

1152 permissions = [ 

1153 ("vet_testsuite", "Can vet a testsuite"), 

1154 ("suppress_testsuite", "Can suppress a testsuite"), 

1155 ] 

1156 

1157 @cached_property 

1158 def __acceptable_statuses__(self): 

1159 return set([r.id for r in self.acceptable_statuses.all()]) 

1160 

1161 def __str__(self): 

1162 return self.name 

1163 

1164 def is_failure(self, status): 

1165 return status.id not in self.__acceptable_statuses__ 

1166 

1167 

1168class TestsuiteRun(models.Model, UserFiltrableMixin): 

1169 # For the FilterMixin. 

1170 filter_objects_to_db = { 

1171 'testsuite_name': FilterObjectStr('testsuite__name', 

1172 'Name of the testsuite that was used for this run'), 

1173 'runconfig': FilterObjectModel(RunConfig, 'runconfig', 'Run configuration the test is part of'), 

1174 'runconfig_name': FilterObjectStr('runconfig__name', 'Name of the run configuration'), 

1175 'runconfig_tag': FilterObjectStr('runconfig__tags__name', 

1176 'Tag associated with the configuration for this run'), 

1177 'runconfig_added_on': FilterObjectDateTime('runconfig__added_on', 

1178 'Date at which the run configuration got created'), 

1179 'runconfig_temporary': FilterObjectBool('runconfig__temporary', 

1180 'Is the run configuration temporary (pre-merge testing)?'), 

1181 'machine_name': FilterObjectStr('machine__name', 'Name of the machine used in this run'), 

1182 'machine_tag': FilterObjectStr('machine__tags__name', 'Tag associated to the machine used in this run'), 

1183 'url': FilterObjectStr('url', 'External URL associated to this testsuite run'), 

1184 'start': FilterObjectDateTime('start', "Local time at witch the run started on the machine"), 

1185 'duration': FilterObjectDuration('duration', 'Duration of the testsuite run'), 

1186 'reported_on': FilterObjectDateTime('reported_on', 

1187 'Date at which the testsuite run got imported in CI Bug Log'), 

1188 'environment': FilterObjectStr('environment', 'Free-text field describing the environment of the machine'), 

1189 'log': FilterObjectStr('log', 'Log of the testsuite run'), 

1190 } 

1191 

1192 testsuite = models.ForeignKey(TestSuite, on_delete=models.CASCADE) 

1193 runconfig = models.ForeignKey(RunConfig, on_delete=models.CASCADE) 

1194 machine = models.ForeignKey(Machine, on_delete=models.CASCADE) 

1195 run_id = models.IntegerField() 

1196 url = models.URLField(null=True, blank=True) 

1197 

1198 start = models.DateTimeField() 

1199 duration = models.DurationField() 

1200 reported_on = models.DateTimeField(auto_now_add=True) 

1201 

1202 environment = models.TextField(blank=True, 

1203 help_text="A human-readable, and machine-parsable definition of the environment. " 

1204 "Make sure the environment contains a header with the format and version.") 

1205 log = models.TextField(blank=True) 

1206 

1207 class Meta: 

1208 constraints = [ 

1209 UniqueConstraint( 

1210 fields=('testsuite', 'runconfig', 'machine', 'run_id'), 

1211 name='unique_testsuite_runconfig_machine_run_id', 

1212 ), 

1213 ] 

1214 ordering = ['start'] 

1215 

1216 def __str__(self): 

1217 return "{} on {} - testsuite run {}".format(self.runconfig.name, self.machine.name, self.run_id) 

1218 

1219 

1220class TextStatus(VettableObjectMixin, ColoredObjectMixin, models.Model, UserFiltrableMixin): 

1221 filter_objects_to_db = { 

1222 'name': FilterObjectStr('name', "Name of the status"), 

1223 'added_on': FilterObjectDateTime('added_on', "Datetime at which the text status was added"), 

1224 } 

1225 testsuite = models.ForeignKey(TestSuite, on_delete=models.CASCADE) 

1226 name = models.CharField(max_length=20) 

1227 

1228 vetted_on = models.DateTimeField(db_index=True, null=True, blank=True, 

1229 help_text="When did the status get ready for pre-merge testing?") 

1230 added_on = models.DateTimeField(auto_now_add=True) 

1231 

1232 color_hex = ColoredObjectMixin.color_hex 

1233 

1234 severity = models.PositiveIntegerField(null=True, blank=True, 

1235 help_text="Define how bad a the status is, from better to worse. " 

1236 "The best possible is 0.") 

1237 

1238 class Meta: 

1239 constraints = [ 

1240 UniqueConstraint(fields=('testsuite', 'name'), name='unique_testsuite_name') 

1241 ] 

1242 verbose_name_plural = "Text Statuses" 

1243 permissions = [ 

1244 ("vet_textstatus", "Can vet a text status"), 

1245 ("suppress_textstatus", "Can suppress a text status"), 

1246 ] 

1247 

1248 @property 

1249 def is_failure(self): 

1250 return self.testsuite.is_failure(self) 

1251 

1252 @property 

1253 def is_notrun(self): 

1254 return self == self.testsuite.notrun_status 

1255 

1256 @property 

1257 def actual_severity(self): 

1258 if self.severity is not None: 

1259 return self.severity 

1260 elif self.is_notrun: 

1261 return 0 

1262 elif not self.is_failure: 

1263 return 1 

1264 else: 

1265 return 2 

1266 

1267 def __str__(self): 

1268 return "{}: {}".format(self.testsuite, self.name) 

1269 

1270 

1271class TestResultAssociatedManager(models.Manager): 

1272 def get_queryset(self): 

1273 return super().get_queryset().prefetch_related('status__testsuite__acceptable_statuses', 

1274 'status', 'ts_run__machine', 

1275 'ts_run__machine__tags', 

1276 'ts_run__runconfig__tags', 

1277 'test') 

1278 

1279 

1280class TestResult(models.Model, UserFiltrableMixin): 

1281 # For the FilterMixin. 

1282 filter_objects_to_db = { 

1283 'runconfig': FilterObjectModel(RunConfig, 'ts_run__runconfig', 'Run configuration the test is part of'), 

1284 'runconfig_name': FilterObjectStr('ts_run__runconfig__name', 'Name of the run configuration'), 

1285 'runconfig_tag': FilterObjectStr('ts_run__runconfig__tags__name', 

1286 'Tag associated with the configuration used for this test execution'), 

1287 'runconfig_added_on': FilterObjectDateTime('ts_run__runconfig__added_on', 

1288 'Date at which the run configuration got created'), 

1289 'runconfig_temporary': FilterObjectBool('ts_run__runconfig__temporary', 

1290 'Is the run configuration temporary, like for pre-merge testing?'), 

1291 'build_name': FilterObjectStr('ts_run__runconfig__builds__name', 

1292 'Name of the build for a component used for this test execution'), 

1293 'build_added_on': FilterObjectDateTime('ts_run__runconfig__builds__added_on', 

1294 'Date at which the build was added'), 

1295 'component_name': FilterObjectStr('ts_run__runconfig__builds__component__name', 

1296 'Name of a component used for this test execution'), 

1297 'machine_name': FilterObjectStr('ts_run__machine__name', 'Name of the machine used for this result'), 

1298 'machine_tag': FilterObjectStr('ts_run__machine__tags__name', 

1299 'Tag associated to the machine used in this run'), 

1300 'status_name': FilterObjectStr('status__name', 'Name of the resulting status (pass/fail/crash/...)'), 

1301 'testsuite_name': FilterObjectStr('status__testsuite__name', 

1302 'Name of the testsuite that contains this test'), 

1303 'test_name': FilterObjectStr('test__name', 'Name of the test'), 

1304 'test_added_on': FilterObjectDateTime('test__added_on', 'Date at which the test got added'), 

1305 'manually_filed_on': FilterObjectDateTime('known_failure__manually_associated_on', 

1306 'Date at which the failure got manually associated to an issue'), 

1307 'ifa_id': FilterObjectInteger('known_failure__matched_ifa_id', 

1308 'ID of the associated filter that matched the failure'), 

1309 'issue_id': FilterObjectInteger('known_failure__matched_ifa__issue_id', 

1310 'ID of the issue associated to the failure'), 

1311 'issue_expected': FilterObjectBool('known_failure__matched_ifa__issue__expected', 

1312 'Is the issue associated to the failure marked as expected?'), 

1313 'filter_description': FilterObjectStr('known_failure__matched_ifa__issue__filters__description', 

1314 'Description of what the filter associated to the failure'), 

1315 'filter_runconfig_tag_name': 

1316 FilterObjectStr('known_failure__matched_ifa__issue__filters__tags__name', 

1317 'Run configuration tag matched by the filter associated to the failure'), 

1318 'filter_machine_tag_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machine_tags__name', 

1319 'Machine tag matched by the filter associated to the failure'), 

1320 'filter_machine_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machines__name', 

1321 'Name of a machine matched by the filter associated to the failure'), 

1322 'filter_test_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__tests__name', 

1323 'Name of a test matched by the filter associated to the failure'), 

1324 'filter_status_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__statuses__name', 

1325 'Status matched by the filter associated to the failure'), 

1326 'filter_stdout_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stdout_regex', 

1327 'Standard output regex used by the filter associated to the failure'), 

1328 'filter_stderr_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stderr_regex', 

1329 'Standard error regex used by the filter associated to the failure'), 

1330 'filter_dmesg_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__dmesg_regex', 

1331 'Regex for dmesg used by the filter associated to the failure'), 

1332 'filter_added_on': FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__added_on', 

1333 'Date at which the filter associated to the failure was added on to its issue'), # noqa 

1334 'filter_covers_from': 

1335 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__covers_from', 

1336 'Date of the first failure covered by the filter associated to the failure'), 

1337 'filter_deleted_on': 

1338 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__deleted_on', 

1339 'Date at which the filter was removed from the issue associated to the failure'), 

1340 'filter_runconfigs_covered_count': 

1341 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

1342 'Amount of run configurations covered by the filter associated to the failure'), 

1343 'filter_runconfigs_affected_count': 

1344 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

1345 'Amount of run configurations affected by the filter associated to the failure'), 

1346 'filter_last_seen': 

1347 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__last_seen', 

1348 'Date at which the filter matching this failure was last seen'), 

1349 'filter_last_seen_runconfig_name': 

1350 FilterObjectStr('known_failure__matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

1351 'Run configuration which last matched the filter associated to the failure'), 

1352 'bug_tracker_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__name', 

1353 'Name of the tracker which holds the bug associated to this failure'), 

1354 'bug_tracker_short_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__short_name', 

1355 'Short name of the tracker which holds the bug associated to this failure'), # noqa 

1356 'bug_tracker_type': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__tracker_type', 

1357 'Type of the tracker which holds the bug associated to this failure'), 

1358 'bug_id': FilterObjectStr('known_failure__matched_ifa__issue__bugs__bug_id', 

1359 'ID of the bug associated to this failure'), 

1360 'bug_title': FilterObjectStr('known_failure__matched_ifa__issue__bugs__title', 

1361 'Title of the bug associated to this failure'), 

1362 'bug_created_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__created', 

1363 'Date at which the bug associated to this failure was created'), 

1364 'bug_closed_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__closed', 

1365 'Date at which the bug associated to this failure was closed'), 

1366 'bug_creator_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__full_name', 

1367 'Name of the creator of the bug associated to this failure'), 

1368 'bug_creator_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__email', 

1369 'Email address of the creator of the bug associated to this failure'), 

1370 'bug_assignee_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__full_name', 

1371 'Name of the assignee of the bug associated to this failure'), 

1372 'bug_assignee_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__email', 

1373 'Email address of the assignee of the bug associated to this failure'), 

1374 'bug_product': FilterObjectStr('known_failure__matched_ifa__issue__bugs__product', 

1375 'Product of the bug associated to this failure'), 

1376 'bug_component': FilterObjectStr('known_failure__matched_ifa__issue__bugs__component', 

1377 'Component of the bug associated to this failure'), 

1378 'bug_priority': FilterObjectStr('known_failure__matched_ifa__issue__bugs__priority', 

1379 'Priority of the bug associated to this failure'), 

1380 'bug_features': FilterObjectStr('known_failure__matched_ifa__issue__bugs__features', 

1381 'Features of the bug associated to this failure (coma-separated list)'), 

1382 'bug_platforms': FilterObjectStr('known_failure__matched_ifa__issue__bugs__platforms', 

1383 'Platforms of the bug associated to this failure (coma-separated list)'), 

1384 'bug_status': FilterObjectStr('known_failure__matched_ifa__issue__bugs__status', 

1385 'Status of the bug associated to this failure'), 

1386 'bug_tags': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tags', 

1387 'Tags/labels on the bug associated to this failure (coma-separated list)'), 

1388 'url': FilterObjectStr('url', 'External URL of this test result'), 

1389 'start': FilterObjectDateTime('start', 'Date at which this test started being executed'), 

1390 'duration': FilterObjectDuration('duration', 'Time it took to execute the test'), 

1391 'command': FilterObjectStr('command', 'Command used to execute the test'), 

1392 'stdout': FilterObjectStr('stdout', 'Standard output of the test execution'), 

1393 'stderr': FilterObjectStr('stderr', 'Error output of the test execution'), 

1394 'dmesg': FilterObjectStr('dmesg', 'Kernel logs of the test execution'), 

1395 } 

1396 

1397 test = models.ForeignKey(Test, on_delete=models.CASCADE) 

1398 ts_run = models.ForeignKey(TestsuiteRun, on_delete=models.CASCADE) 

1399 status = models.ForeignKey(TextStatus, on_delete=models.CASCADE) 

1400 

1401 url = models.URLField(null=True, blank=True, max_length=300) 

1402 

1403 start = models.DateTimeField() 

1404 duration = models.DurationField() 

1405 

1406 command = models.CharField(max_length=500) 

1407 stdout = models.TextField(null=True) 

1408 stderr = models.TextField(null=True) 

1409 dmesg = models.TextField(null=True) 

1410 

1411 objects = models.Manager() 

1412 objects_ready_for_matching = TestResultAssociatedManager() 

1413 

1414 @cached_property 

1415 def is_failure(self): 

1416 return self.status.testsuite.is_failure(self.status) 

1417 

1418 @cached_property 

1419 def known_failures_cached(self): 

1420 return self.known_failures.all() 

1421 

1422 def __str__(self): 

1423 return "{} on {} - {}: ({})".format(self.ts_run.runconfig.name, self.ts_run.machine.name, 

1424 self.test.name, self.status) 

1425 

1426# TODO: Support benchmarks too by creating BenchmarkResult (test, run, environment, ...) 

1427 

1428# Issues 

1429 

1430 

1431class IssueFilter(models.Model): 

1432 description = models.CharField(max_length=255, 

1433 help_text="Short description of what the filter matches!") 

1434 

1435 tags = models.ManyToManyField(RunConfigTag, blank=True, 

1436 help_text="The result's run should have at least one of these tags " 

1437 "(leave empty to ignore tags)") 

1438 machine_tags = models.ManyToManyField(MachineTag, blank=True, 

1439 help_text="The result's machine should have one of these tags " 

1440 "(leave empty to ignore machines)") 

1441 machines = models.ManyToManyField(Machine, blank=True, 

1442 help_text="The result's machine should be one of these machines " 

1443 "(extends the set of machines selected by the machine tags, " 

1444 "leave empty to ignore machines)") 

1445 tests = models.ManyToManyField(Test, blank=True, 

1446 help_text="The result's machine should be one of these tests " 

1447 "(leave empty to ignore tests)") 

1448 statuses = models.ManyToManyField(TextStatus, blank=True, 

1449 help_text="The result's status should be one of these (leave empty to " 

1450 "ignore results)") 

1451 

1452 stdout_regex = models.CharField(max_length=1000, blank=True, 

1453 help_text="The result's stdout field must contain a substring matching this " 

1454 "regular expression (leave empty to ignore stdout)") 

1455 stderr_regex = models.CharField(max_length=1000, blank=True, 

1456 help_text="The result's stderr field must contain a substring matching this " 

1457 "regular expression (leave empty to ignore stderr)") 

1458 dmesg_regex = models.CharField(max_length=1000, blank=True, 

1459 help_text="The result's dmesg field must contain a substring matching this " 

1460 "regular expression (leave empty to ignore dmesg)") 

1461 

1462 added_on = models.DateTimeField(auto_now_add=True) 

1463 hidden = models.BooleanField(default=False, db_index=True, help_text="Do not show this filter in filter lists") 

1464 user_query = models.TextField(blank=True, null=True, help_text="User query representation of filter") 

1465 

1466 def delete(self): 

1467 self.hidden = True 

1468 

1469 @cached_property 

1470 def tags_cached(self): 

1471 return set(self.tags.all()) 

1472 

1473 @cached_property 

1474 def tags_ids_cached(self): 

1475 return set([t.id for t in self.tags_cached]) 

1476 

1477 @cached_property 

1478 def __machines_cached__(self): 

1479 return set(self.machines.all()) 

1480 

1481 @cached_property 

1482 def __machine_tags_cached__(self): 

1483 return set(self.machine_tags.all()) 

1484 

1485 @cached_property 

1486 def machines_cached(self): 

1487 machines = self.__machines_cached__.copy() 

1488 for machine in Machine.objects.filter(tags__in=self.__machine_tags_cached__): 

1489 machines.add(machine) 

1490 return machines 

1491 

1492 @cached_property 

1493 def machines_ids_cached(self): 

1494 return set([m.id for m in self.machines_cached]) 

1495 

1496 @cached_property 

1497 def tests_cached(self): 

1498 return set(self.tests.all()) 

1499 

1500 @cached_property 

1501 def tests_ids_cached(self): 

1502 return set([m.id for m in self.tests_cached]) 

1503 

1504 @cached_property 

1505 def statuses_cached(self): 

1506 return set(self.statuses.all()) 

1507 

1508 @cached_property 

1509 def statuses_ids_cached(self): 

1510 return set([s.id for s in self.statuses_cached]) 

1511 

1512 @cached_property 

1513 def stdout_regex_cached(self): 

1514 return re.compile(self.stdout_regex, re.DOTALL) 

1515 

1516 @cached_property 

1517 def stderr_regex_cached(self): 

1518 return re.compile(self.stderr_regex, re.DOTALL) 

1519 

1520 @cached_property 

1521 def dmesg_regex_cached(self): 

1522 return re.compile(self.dmesg_regex, re.DOTALL) 

1523 

1524 @cached_property 

1525 def covered_results(self): 

1526 return QueryParser( 

1527 TestResult, self.equivalent_user_query, ignore_fields=["stdout", "stderr", "dmesg", "status_name"] 

1528 ).objects 

1529 

1530 @cached_property 

1531 def __covers_function(self): 

1532 parser = QueryParserPython( 

1533 TestResult, self.equivalent_user_query, ignore_fields=["stdout", "stderr", "dmesg", "status_name"] 

1534 ) 

1535 if not parser.is_valid: 

1536 raise ValueError("Invalid cover function", parser.error) 

1537 return parser.matching_fn 

1538 

1539 def covers(self, result): 

1540 try: 

1541 return self.__covers_function(result) 

1542 except ValueError as err: 

1543 print(f"Couldn't cover issue filter {self.pk} for result {result}: {err}") 

1544 return False 

1545 

1546 @cached_property 

1547 def matched_results(self): 

1548 return QueryParser(TestResult, self.equivalent_user_query).objects 

1549 

1550 @property 

1551 def matched_unknown_failures(self): 

1552 return QueryParser(UnknownFailure, self.equivalent_user_query).objects 

1553 

1554 @cached_property 

1555 def __matches_function(self): 

1556 parser = QueryParserPython(TestResult, self.equivalent_user_query) 

1557 if not parser.is_valid: 

1558 raise ValueError("Invalid match function", parser.error) 

1559 return parser.matching_fn 

1560 

1561 def matches(self, result, skip_cover_test=False): 

1562 try: 

1563 return self.__matches_function(result) 

1564 except ValueError as err: 

1565 print(f"Couldn't match issue filter {self.pk} for result {result}: {err}") 

1566 return False 

1567 

1568 @transaction.atomic 

1569 def replace(self, new_filter, user): 

1570 # Go through all the issues that currently use this filter 

1571 for e in IssueFilterAssociated.objects.filter(deleted_on=None, filter=self): 

1572 e.issue.replace_filter(self, new_filter, user) 

1573 

1574 # Hide this filter now and only keep it for archive purposes 

1575 self.delete() 

1576 

1577 def _to_user_query(self, covers=True, matches=True): 

1578 query = [] 

1579 

1580 if covers: 

1581 if len(self.tags_cached) > 0: 

1582 query.append('runconfig_tag IS IN ["{}"]'.format('", "'.join([t.name for t in self.tags_cached]))) 

1583 

1584 if len(self.__machines_cached__) > 0 or len(self.__machine_tags_cached__) > 0: 

1585 if len(self.__machines_cached__) > 0: 

1586 machines = [m.name for m in self.__machines_cached__] 

1587 machines_query = 'machine_name IS IN ["{}"]'.format('", "'.join(machines)) 

1588 if len(self.__machine_tags_cached__) > 0: 

1589 tags = [t.name for t in self.__machine_tags_cached__] 

1590 machine_tags_query = 'machine_tag IS IN ["{}"]'.format('", "'.join(tags)) 

1591 

1592 if len(self.__machines_cached__) > 0 and len(self.__machine_tags_cached__) > 0: 

1593 query.append("({} OR {})".format(machines_query, machine_tags_query)) 

1594 elif len(self.__machines_cached__) > 0: 

1595 query.append(machines_query) 

1596 else: 

1597 query.append(machine_tags_query) 

1598 

1599 if len(self.tests_cached) > 0: 

1600 tests_query = [] 

1601 

1602 # group the tests by testsuite 

1603 testsuites = defaultdict(set) 

1604 for test in self.tests_cached: 

1605 testsuites[test.testsuite].add(test) 

1606 

1607 # create the sub-queries 

1608 for testsuite in testsuites: 

1609 subquery = '(testsuite_name = "{}" AND test_name IS IN ["{}"])' 

1610 tests_query.append(subquery.format(testsuite.name, 

1611 '", "'.join([t.name for t in testsuites[testsuite]]))) 

1612 query.append("({})".format(" OR ".join(tests_query))) 

1613 

1614 if matches: 

1615 if len(self.statuses_cached) > 0: 

1616 status_query = [] 

1617 

1618 # group the statuses by testsuite 

1619 testsuites = defaultdict(set) 

1620 for status in self.statuses_cached: 

1621 testsuites[status.testsuite].add(status) 

1622 

1623 # create the sub-queries 

1624 for testsuite in testsuites: 

1625 subquery = '(testsuite_name = "{}" AND status_name IS IN ["{}"])' 

1626 status_query.append(subquery.format(testsuite.name, 

1627 '", "'.join([s.name for s in testsuites[testsuite]]))) 

1628 query.append("({})".format(" OR ".join(status_query))) 

1629 

1630 if len(self.stdout_regex) > 0: 

1631 query.append("stdout ~= '{}'".format(self.stdout_regex.replace("'", "\\'"))) 

1632 

1633 if len(self.stderr_regex) > 0: 

1634 query.append("stderr ~= '{}'".format(self.stderr_regex.replace("'", "\\'"))) 

1635 

1636 if len(self.dmesg_regex) > 0: 

1637 query.append("dmesg ~= '{}'".format(self.dmesg_regex.replace("'", "\\'"))) 

1638 

1639 return " AND ".join(query) 

1640 

1641 @cached_property 

1642 def equivalent_user_query(self) -> str: 

1643 if self.user_query: 

1644 return self.user_query 

1645 return self._to_user_query() 

1646 

1647 def __str__(self): 

1648 return self.description 

1649 

1650 

1651class Rate: 

1652 def __init__(self, type_str, affected, total): 

1653 self._type_str = type_str 

1654 self._affected = affected 

1655 self._total = total 

1656 

1657 @property 

1658 def rate(self): 

1659 if self._total > 0: 

1660 return self._affected / self._total 

1661 else: 

1662 return 0 

1663 

1664 def __str__(self): 

1665 return "{} / {} {} ({:.1f}%)".format(self._affected, 

1666 self._total, 

1667 self._type_str, 

1668 self.rate * 100.0) 

1669 

1670 

1671class IssueFilterAssociatedManager(models.Manager): 

1672 def get_queryset(self): 

1673 return super().get_queryset().prefetch_related('filter__tags', 

1674 'filter__machine_tags', 

1675 'filter__machines', 

1676 'filter__tests', 

1677 'filter__statuses', 

1678 'filter') 

1679 

1680 

1681class IssueFilterAssociated(models.Model): 

1682 filter = models.ForeignKey(IssueFilter, on_delete=models.CASCADE) 

1683 issue = models.ForeignKey('Issue', on_delete=models.CASCADE) 

1684 

1685 added_on = models.DateTimeField(auto_now_add=True) 

1686 added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='filter_creator', 

1687 null=True, on_delete=models.SET(get_sentinel_user)) 

1688 

1689 # WARNING: Make sure this is set when archiving the issue 

1690 deleted_on = models.DateTimeField(blank=True, null=True, db_index=True) 

1691 deleted_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='filter_deleter', 

1692 null=True, on_delete=models.SET(get_sentinel_user)) 

1693 

1694 # Statistics cache 

1695 covers_from = models.DateTimeField(default=timezone.now) 

1696 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1697 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

1698 last_seen = models.DateTimeField(null=True, blank=True) 

1699 last_seen_runconfig = models.ForeignKey('RunConfig', null=True, blank=True, on_delete=models.SET_NULL) 

1700 

1701 objects = models.Manager() 

1702 objects_ready_for_matching = IssueFilterAssociatedManager() 

1703 

1704 @property 

1705 def active(self): 

1706 return self.deleted_on is None 

1707 

1708 def delete(self, user, now=None): 

1709 if self.deleted_on is not None: 

1710 return 

1711 

1712 if now is not None: 

1713 self.deleted_on = now 

1714 else: 

1715 self.deleted_on = timezone.now() 

1716 

1717 self.deleted_by = user 

1718 self.save() 

1719 

1720 @cached_property 

1721 def __runfilter_stats_covered__(self): 

1722 objs = RunFilterStatistic.objects.select_related("runconfig") 

1723 

1724 # We want to look for all runs created after either when the filter 

1725 # got associated, since the creation of the first runcfg that contains 

1726 # a failure that retro-actively associated to this issue. Pick the 

1727 # earliest of these two events. 

1728 start_time = self.added_on 

1729 if self.covers_from < start_time: 

1730 start_time = self.covers_from 

1731 

1732 if self.deleted_on is not None: 

1733 return objs.filter(runconfig__added_on__gte=start_time, 

1734 runconfig__added_on__lt=self.deleted_on, 

1735 covered_count__gt=0, 

1736 filter__id=self.filter_id).order_by('-id') 

1737 else: 

1738 return objs.filter(runconfig__added_on__gte=start_time, 

1739 covered_count__gt=0, 

1740 filter__id=self.filter_id).order_by('-id') 

1741 

1742 @cached_property 

1743 def runconfigs_covered(self): 

1744 return set([r.runconfig for r in self.__runfilter_stats_covered__]) 

1745 

1746 @cached_property 

1747 def runconfigs_affected(self): 

1748 return set([r.runconfig for r in self.__runfilter_stats_covered__ if r.matched_count > 0]) 

1749 

1750 @property 

1751 def covered_results(self): 

1752 q = self.filter.covered_results.filter(ts_run__runconfig__added_on__gte=self.covers_from) 

1753 return q.prefetch_related('ts_run', 'ts_run__runconfig') 

1754 

1755 def _add_missing_stats(self): 

1756 # Find the list of runconfig we have stats for 

1757 runconfigs_done = RunFilterStatistic.objects.filter(filter=self.filter).values_list('runconfig', flat=True) 

1758 

1759 # Get the list of results, excluding the ones coming from runconfigs we already have 

1760 stats = dict() 

1761 results = ( 

1762 self.covered_results.exclude(ts_run__runconfig__id__in=runconfigs_done) 

1763 .filter(ts_run__runconfig__temporary=False) 

1764 .only("id", "ts_run") 

1765 ) 

1766 for result in results: 

1767 runconfig = result.ts_run.runconfig 

1768 fs = stats.get(runconfig) 

1769 if fs is None: 

1770 stats[runconfig] = fs = RunFilterStatistic(filter=self.filter, runconfig=runconfig, 

1771 matched_count=0, covered_count=0) 

1772 

1773 fs.covered_count += 1 

1774 

1775 # Now that we know which results are covered, we just need to refine our 

1776 # query to also check if they matched. 

1777 # 

1778 # To avoid asking the database to re-do the coverage test, just use the 

1779 # list of ids we got previously 

1780 query = QueryParser(TestResult, self.filter._to_user_query(covers=False, matches=True)).objects 

1781 query = query.filter(id__in=[r.id for r in results]).only('ts_run').prefetch_related('ts_run__runconfig') 

1782 for result in query: 

1783 stats[result.ts_run.runconfig].matched_count += 1 

1784 

1785 # Save the statistics objects 

1786 for fs in stats.values(): 

1787 fs.save() 

1788 

1789 def update_statistics(self): 

1790 # drop all the caches 

1791 try: 

1792 del self.__runfilter_stats_covered__ 

1793 del self.runconfigs_covered 

1794 del self.runconfigs_affected 

1795 except AttributeError: 

1796 # Ignore the error if the cache had not been accessed before 

1797 pass 

1798 

1799 req = KnownFailure.objects.filter(matched_ifa=self, result__ts_run__runconfig__temporary=False) 

1800 req = req.order_by("result__ts_run__runconfig__added_on") 

1801 oldest_failure = req.values_list('result__ts_run__runconfig__added_on', flat=True).first() 

1802 if oldest_failure is not None: 

1803 self.covers_from = oldest_failure 

1804 

1805 # get the list of runconfigs needing update 

1806 self._add_missing_stats() 

1807 

1808 self.runconfigs_covered_count = len(self.runconfigs_covered) 

1809 self.runconfigs_affected_count = len(self.runconfigs_affected) 

1810 

1811 # Find when the issue was last seen 

1812 for stats in self.__runfilter_stats_covered__: 

1813 if stats.matched_count > 0: 

1814 self.last_seen = stats.runconfig.added_on 

1815 self.last_seen_runconfig = stats.runconfig 

1816 break 

1817 

1818 # Update the statistics atomically in the DB 

1819 cur_ifa = IssueFilterAssociated.objects.filter(id=self.id) 

1820 cur_ifa.update(covers_from=self.covers_from, 

1821 runconfigs_covered_count=self.runconfigs_covered_count, 

1822 runconfigs_affected_count=self.runconfigs_affected_count, 

1823 last_seen=self.last_seen, 

1824 last_seen_runconfig=self.last_seen_runconfig) 

1825 

1826 @property 

1827 def failure_rate(self): 

1828 return Rate("runs", self.runconfigs_affected_count, self.runconfigs_covered_count) 

1829 

1830 @property 

1831 def activity_period(self): 

1832 added_by = " by {}".format(render_to_string("CIResults/basic/user.html", 

1833 {"user": self.added_by}).strip()) if self.added_by else "" 

1834 deleted_by = " by {}".format(render_to_string("CIResults/basic/user.html", 

1835 {"user": self.deleted_by}).strip()) if self.deleted_by else "" 

1836 

1837 if not self.active: 

1838 s = "Added {}{}, removed {}{} (was active for {})" 

1839 return s.format(naturaltime(self.added_on), added_by, 

1840 naturaltime(self.deleted_on), deleted_by, 

1841 timesince(self.added_on, self.deleted_on)) 

1842 else: 

1843 return "Added {}{}".format(naturaltime(self.added_on), added_by) 

1844 

1845 def __str__(self): 

1846 if self.deleted_on is not None: 

1847 delete_on = " - deleted on {}".format(self.deleted_on) 

1848 else: 

1849 delete_on = "" 

1850 

1851 return "{} on {}{}".format(self.filter.description, self.issue, delete_on) 

1852 

1853 

1854class Issue(models.Model, UserFiltrableMixin): 

1855 filter_objects_to_db = { 

1856 'filter_description': FilterObjectStr('filters__description', 

1857 'Description of what the filter matches'), 

1858 'filter_runconfig_tag_name': FilterObjectStr('filters__tags__name', 

1859 'Run configuration tag matched by the filter'), 

1860 'filter_machine_tag_name': FilterObjectStr('filters__machine_tags__name', 

1861 'Machine tag matched by the filter'), 

1862 'filter_machine_name': FilterObjectStr('filters__machines__name', 

1863 'Name of a machine matched by the filter'), 

1864 'filter_test_name': FilterObjectStr('filters__tests__name', 

1865 'Name of a test matched by the filter'), 

1866 'filter_status_name': FilterObjectStr('filters__statuses__name', 

1867 'Status matched by the filter'), 

1868 'filter_stdout_regex': FilterObjectStr('filters__stdout_regex', 

1869 'Regular expression for the standard output used by the filter'), 

1870 'filter_stderr_regex': FilterObjectStr('filters__stderr_regex', 

1871 'Regular expression for the error output used by the filter'), 

1872 'filter_dmesg_regex': FilterObjectStr('filters__dmesg_regex', 

1873 'Regular expression for the kernel logs used by the filter'), 

1874 'filter_added_on': FilterObjectDateTime('issuefilterassociated__added_on', 

1875 'Date at which the filter was associated to the issue'), 

1876 'filter_covers_from': FilterObjectDateTime('issuefilterassociated__covers_from', 

1877 'Date of the first failure covered by the filter'), 

1878 'filter_deleted_on': FilterObjectDateTime('issuefilterassociated__deleted_on', 

1879 'Date at which the filter was deleted from the issue'), 

1880 'filter_runconfigs_covered_count': FilterObjectInteger('issuefilterassociated__runconfigs_covered_count', 

1881 'Amount of run configurations covered by the filter'), 

1882 'filter_runconfigs_affected_count': FilterObjectInteger('issuefilterassociated__runconfigs_affected_count', 

1883 'Amount of run configurations affected by the filter'), 

1884 'filter_last_seen': FilterObjectDateTime('issuefilterassociated__last_seen', 

1885 'Date at which the filter last matched'), 

1886 'filter_last_seen_runconfig_name': FilterObjectStr('issuefilterassociated__last_seen_runconfig__name', 

1887 'Run configuration which last matched the filter'), 

1888 

1889 'bug_tracker_name': FilterObjectStr('bugs__tracker__name', 

1890 'Name of the tracker hosting the bug associated to the issue'), 

1891 'bug_tracker_short_name': FilterObjectStr('bugs__tracker__short_name', 

1892 'Short name of the tracker hosting the bug associated to the issue'), 

1893 'bug_tracker_type': FilterObjectStr('bugs__tracker__tracker_type', 

1894 'Type of tracker hosting the bug associated to the issue'), 

1895 'bug_id': FilterObjectStr('bugs__bug_id', 

1896 'ID of the bug associated to the issue'), 

1897 'bug_title': FilterObjectStr('bugs__title', 

1898 'Title of the bug associated to the issue'), 

1899 'bug_created_on': FilterObjectDateTime('bugs__created', 

1900 'Date at which the bug associated to the issue was created'), 

1901 'bug_updated_on': FilterObjectDateTime('bugs__updated', 

1902 'Date at which the bug associated to the issue was last updated'), 

1903 'bug_closed_on': FilterObjectDateTime('bugs__closed', 

1904 'Date at which the bug associated to the issue was closed'), 

1905 'bug_creator_name': FilterObjectStr('bugs__creator__person__full_name', 

1906 'Name of the creator of the bug associated to the issue'), 

1907 'bug_creator_email': FilterObjectStr('bugs__creator__person__email', 

1908 'Email address of the creator of the bug associated to the issue'), 

1909 'bug_assignee_name': FilterObjectStr('bugs__assignee__person__full_name', 

1910 'Name of the assignee of the bug associated to the issue'), 

1911 'bug_assignee_email': FilterObjectStr('bugs__assignee__person__email', 

1912 'Email address of the assignee of the bug associated to the issue'), 

1913 'bug_product': FilterObjectStr('bugs__product', 'Product of the bug associated to the issue'), 

1914 'bug_component': FilterObjectStr('bugs__component', 'Component of the bug associated to the issue'), 

1915 'bug_priority': FilterObjectStr('bugs__priority', 'Priority of the bug associated to the issue'), 

1916 'bug_features': FilterObjectStr('bugs__features', 

1917 'Features of the bug associated to the issue (coma-separated list)'), 

1918 'bug_platforms': FilterObjectStr('bugs__platforms', 

1919 'Platforms of the bug associated to the issue (coma-separated list)'), 

1920 'bug_status': FilterObjectStr('bugs__status', 

1921 'Status of the bug associated to the issue'), 

1922 'bug_severity': FilterObjectStr('bugs__severity', 'Severity of the bug associated to the issue'), 

1923 'bug_tags': FilterObjectStr('bugs__tags', 

1924 'Tags/labels on the bug associated to this issue (coma-separated list)'), 

1925 

1926 'description': FilterObjectStr('description', 'Free-hand text associated to the issue by the bug filer'), 

1927 'filer_email': FilterObjectStr('filer', 'Email address of the person who filed the issue (DEPRECATED)'), 

1928 

1929 'id': FilterObjectInteger('id', 'Id of the issue'), 

1930 

1931 'added_on': FilterObjectDateTime('added_on', 'Date at which the issue was created'), 

1932 'added_by': FilterObjectStr('added_by__username', 'Username of the person who filed the issue'), 

1933 'archived_on': FilterObjectDateTime('archived_on', 'Date at which the issue was archived'), 

1934 'archived_by': FilterObjectStr('archived_by__username', 'Username of the person who archived the issue'), 

1935 'expected': FilterObjectBool('expected', 'Is the issue expected?'), 

1936 'runconfigs_covered_count': FilterObjectInteger('runconfigs_covered_count', 

1937 'Amount of run configurations covered by the issue'), 

1938 'runconfigs_affected_count': FilterObjectInteger('runconfigs_affected_count', 

1939 'Amount of run configurations affected by the issue'), 

1940 'last_seen': FilterObjectDateTime('last_seen', 'Date at which the issue was last seen'), 

1941 'last_seen_runconfig_name': FilterObjectStr('last_seen_runconfig__name', 

1942 'Run configuration which last reproduced the issue'), 

1943 } 

1944 

1945 filters = models.ManyToManyField(IssueFilter, through="IssueFilterAssociated") 

1946 bugs = models.ManyToManyField(Bug) 

1947 

1948 description = models.TextField(blank=True) 

1949 filer = models.EmailField() # DEPRECATED 

1950 

1951 added_on = models.DateTimeField(auto_now_add=True) 

1952 added_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='issue_creator', 

1953 null=True, on_delete=models.SET(get_sentinel_user)) 

1954 

1955 archived_on = models.DateTimeField(blank=True, null=True, db_index=True) 

1956 archived_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='issue_archiver', 

1957 null=True, on_delete=models.SET(get_sentinel_user)) 

1958 

1959 expected = models.BooleanField(default=False, db_index=True, 

1960 help_text="Is this issue expected and should be considered an active issue?") 

1961 

1962 # Statistics cache 

1963 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1964 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

1965 last_seen = models.DateTimeField(null=True, blank=True) 

1966 last_seen_runconfig = models.ForeignKey('RunConfig', null=True, blank=True, on_delete=models.SET_NULL) 

1967 

1968 class Meta: 

1969 permissions = [ 

1970 ("archive_issue", "Can archive issues"), 

1971 ("restore_issue", "Can restore issues"), 

1972 

1973 ("hide_issue", "Can hide / mark as un-expected"), 

1974 ("show_issue", "Can show / mark as as expected"), 

1975 ] 

1976 

1977 @property 

1978 def archived(self): 

1979 return self.archived_on is not None 

1980 

1981 def hide(self): 

1982 self.expected = True 

1983 self.save() 

1984 

1985 def show(self): 

1986 self.expected = False 

1987 self.save() 

1988 

1989 @cached_property 

1990 def active_filters(self): 

1991 if self.archived: 

1992 deleted_on = self.archived_on 

1993 else: 

1994 deleted_on = None 

1995 

1996 if hasattr(self, 'ifas_cached'): 

1997 ifas = filter(lambda i: i.deleted_on == deleted_on, self.ifas_cached) 

1998 return sorted(ifas, reverse=True, key=lambda i: i.id) 

1999 else: 

2000 ifas = IssueFilterAssociated.objects.filter(issue=self, 

2001 deleted_on=deleted_on) 

2002 return ifas.select_related("filter").order_by('-id') 

2003 

2004 @cached_property 

2005 def all_filters(self): 

2006 return IssueFilterAssociated.objects.filter(issue=self).select_related('filter').order_by('id') 

2007 

2008 @property 

2009 def past_filters(self): 

2010 return [ifa for ifa in self.all_filters if ifa.deleted_on != self.archived_on] 

2011 

2012 @cached_property 

2013 def bugs_cached(self): 

2014 # HACK: Sort by decreasing ID in python so as we can prefetch the bugs 

2015 # in the main view, saving as many SQL requests as we have bugs 

2016 return sorted(self.bugs.all(), reverse=True, key=lambda b: b.id) 

2017 

2018 @cached_property 

2019 def covers_from(self): 

2020 return min([self.added_on] + [min(ifa.added_on, ifa.covers_from) for ifa in self.all_filters]) 

2021 

2022 @cached_property 

2023 def __runfilter_stats_covered__(self): 

2024 filters = [e.filter for e in self.all_filters] 

2025 objs = RunFilterStatistic.objects.select_related("runconfig") 

2026 objs = objs.filter(runconfig__added_on__gte=self.covers_from, 

2027 covered_count__gt=0, 

2028 filter__in=filters).order_by("-runconfig__added_on") 

2029 if self.archived: 

2030 objs = objs.filter(runconfig__added_on__lt=self.archived_on) 

2031 

2032 return objs 

2033 

2034 @cached_property 

2035 def runconfigs_covered(self): 

2036 return set([r.runconfig for r in self.__runfilter_stats_covered__]) 

2037 

2038 @cached_property 

2039 def runconfigs_affected(self): 

2040 # Go through all the RunFilterStats covered by this issue and add runs 

2041 # to the set of affected ones 

2042 runconfigs_affected = set() 

2043 for runfilter in self.__runfilter_stats_covered__: 

2044 if runfilter.matched_count > 0: 

2045 runconfigs_affected.add(runfilter.runconfig) 

2046 

2047 return runconfigs_affected 

2048 

2049 def update_statistics(self): 

2050 self.runconfigs_covered_count = len(self.runconfigs_covered) 

2051 self.runconfigs_affected_count = len(self.runconfigs_affected) 

2052 

2053 # Find when the issue was last seen 

2054 for stats in self.__runfilter_stats_covered__: 

2055 if stats.matched_count > 0: 

2056 self.last_seen = stats.runconfig.added_on 

2057 self.last_seen_runconfig = stats.runconfig 

2058 break 

2059 

2060 # Update the statistics atomically in the DB 

2061 cur_issue = Issue.objects.filter(id=self.id) 

2062 cur_issue.update(runconfigs_covered_count=self.runconfigs_covered_count, 

2063 runconfigs_affected_count=self.runconfigs_affected_count, 

2064 last_seen=self.last_seen, 

2065 last_seen_runconfig=self.last_seen_runconfig) 

2066 

2067 @property 

2068 def failure_rate(self): 

2069 return Rate("runs", self.runconfigs_affected_count, self.runconfigs_covered_count) 

2070 

2071 def matches(self, result): 

2072 if self.archived: 

2073 return False 

2074 

2075 for e in IssueFilterAssociated.objects_ready_for_matching.filter(deleted_on=None): 

2076 if e.filter.matches(result): 

2077 return True 

2078 return False 

2079 

2080 def archive(self, user): 

2081 if self.archived: 

2082 raise ValueError("The issue is already archived") 

2083 

2084 with transaction.atomic(): 

2085 now = timezone.now() 

2086 for e in IssueFilterAssociated.objects.filter(issue=self): 

2087 e.delete(user, now) 

2088 self.archived_on = now 

2089 self.archived_by = user 

2090 self.save() 

2091 

2092 # Post a comment 

2093 self.render_and_leave_comment_on_all_bugs("CIResults/issue_archived.txt", issue=self) 

2094 

2095 def restore(self): 

2096 if not self.archived: 

2097 raise ValueError("The issue is not currently archived") 

2098 

2099 # re-add all the filters that used to be associated 

2100 with transaction.atomic(): 

2101 for e in IssueFilterAssociated.objects.filter(issue=self, deleted_on=self.archived_on): 

2102 self.__filter_add__(e.filter, e.added_by) 

2103 

2104 # Mark the issue as not archived anymore before saving the changes 

2105 self.archived_on = None 

2106 self.archived_by = None 

2107 self.save() 

2108 

2109 # Now update our statistics since we possibly re-assigned some new failures 

2110 self.update_statistics() 

2111 

2112 # Post a comment 

2113 self.render_and_leave_comment_on_all_bugs("CIResults/issue_restored.txt", issue=self) 

2114 

2115 @transaction.atomic 

2116 def set_bugs(self, bugs): 

2117 if self.archived: 

2118 raise ValueError("The issue is archived, and thus read-only") 

2119 

2120 # Let's simply delete all the bugs before adding them back 

2121 self.bugs.clear() 

2122 

2123 for bug in bugs: 

2124 # Make sure the bug exists in the database first 

2125 if bug.id is None: 

2126 bug.save() 

2127 

2128 # Add it to the relation 

2129 self.bugs.add(bug) 

2130 

2131 # Get rid of the cached bugs 

2132 try: 

2133 del self.bugs_cached 

2134 except AttributeError: 

2135 # Ignore the error if the cached had not been accessed before 

2136 pass 

2137 

2138 def _assign_to_known_failures(self, unknown_failures, ifa): 

2139 now = timezone.now() 

2140 new_matched_failures = [] 

2141 for failure in unknown_failures: 

2142 filing_delay = now - failure.result.ts_run.reported_on 

2143 kf = KnownFailure.objects.create(result=failure.result, matched_ifa=ifa, 

2144 manually_associated_on=now, 

2145 filing_delay=filing_delay) 

2146 new_matched_failures.append(kf) 

2147 failure.delete() 

2148 

2149 ifa.update_statistics() 

2150 

2151 return new_matched_failures 

2152 

2153 def __filter_add__(self, filter, user): 

2154 # Make sure the filter exists in the database first 

2155 if filter.id is None: 

2156 filter.save() 

2157 

2158 # Create the association between the filter and the issue 

2159 ifa = IssueFilterAssociated.objects.create(filter=filter, issue=self, added_by=user) 

2160 

2161 # Go through the untracked issues and check if the filter matches any of 

2162 # them. Also include the unknown failures from temporary runs. 

2163 matched_unknown_failures = ( 

2164 filter.matched_unknown_failures.select_related("result") 

2165 .prefetch_related("result__ts_run") 

2166 .defer("result__stdout", "result__stderr", "result__dmesg") 

2167 ) 

2168 return self._assign_to_known_failures(matched_unknown_failures, ifa) 

2169 

2170 def render_and_leave_comment_on_all_bugs(self, template: str, is_new: bool = False, /, **template_kwargs) -> None: 

2171 basic_comment: str = render_to_string(template, template_kwargs) 

2172 basic_comment += "" # Add an empty string to get a string instead of safetext 

2173 

2174 try: 

2175 for bug in self.bugs_cached: 

2176 if is_new and bug.first_seen_in: 

2177 template_kwargs["first_seen_in"] = bug.first_seen_in.name 

2178 comment: str = render_to_string(template, template_kwargs) 

2179 comment += "" # Add an empty string to get a string instead of safetext 

2180 else: 

2181 comment = basic_comment 

2182 bug.add_comment(comment) 

2183 except Exception: # pragma: no cover 

2184 traceback.print_exc() # pragma: no cover 

2185 

2186 def replace_filter(self, old_filter, new_filter, user): 

2187 if self.archived: 

2188 raise ValueError("The issue is archived, and thus read-only") 

2189 

2190 with transaction.atomic(): 

2191 # First, add the new filter 

2192 failures = self.__filter_add__(new_filter, user) 

2193 new_matched_failures = [f for f in failures if not f.result.ts_run.runconfig.temporary] 

2194 

2195 # Delete all active associations of the old filter 

2196 assocs = IssueFilterAssociated.objects.filter(deleted_on=None, filter=old_filter) 

2197 for e in assocs: 

2198 e.delete(user, timezone.now()) 

2199 

2200 # Now update our statistics since we possibly re-assigned some new failures 

2201 self.update_statistics() 

2202 

2203 # Post a comment on the bugs associated to this issue if something changed 

2204 if (old_filter.description != new_filter.description or 

2205 old_filter.equivalent_user_query != new_filter.equivalent_user_query or 

2206 len(new_matched_failures) > 0): 

2207 self.render_and_leave_comment_on_all_bugs( 

2208 "CIResults/issue_replace_filter_comment.txt", 

2209 issue=self, 

2210 old_filter=old_filter, 

2211 new_filter=new_filter, 

2212 new_matched_failures=new_matched_failures, 

2213 user=user, 

2214 ) 

2215 

2216 def set_filters(self, filters, user): 

2217 if self.archived: 

2218 raise ValueError("The issue is archived, and thus read-only") 

2219 

2220 with transaction.atomic(): 

2221 removed_ifas = set() 

2222 new_filters = dict() 

2223 

2224 # Query the set of issues that we currently have 

2225 assocs = IssueFilterAssociated.objects.filter(deleted_on=None, issue=self) 

2226 

2227 # First, "delete" all the filters that are not in the new set 

2228 now = timezone.now() 

2229 for e in assocs: 

2230 if e.filter not in filters: 

2231 e.delete(user, now) 

2232 removed_ifas.add(e) 

2233 

2234 # Now, let's add all the new ones 

2235 cur_filters_ids = set([e.filter.id for e in assocs]) 

2236 for filter in filters: 

2237 if filter.id not in cur_filters_ids: 

2238 new_filters[filter] = self.__filter_add__(filter, user) 

2239 

2240 # Now update our statistics since we possibly re-assigned some new failures 

2241 self.update_statistics() 

2242 

2243 # Get rid of the cached filters 

2244 try: 

2245 del self.active_filters 

2246 except AttributeError: 

2247 # Ignore the error if the cache had not been accessed before 

2248 pass 

2249 

2250 # Add `first_seen_in` field in all bugs associated to this issue 

2251 for bug in self.bugs_cached: 

2252 bug.add_first_seen_in(self, True) 

2253 

2254 # Post a comment on the bugs associated to this issue 

2255 if len(removed_ifas) > 0 or len(new_filters) > 0: 

2256 self.render_and_leave_comment_on_all_bugs( 

2257 "CIResults/issue_set_filters_comment.txt", 

2258 True, 

2259 issue=self, 

2260 removed_ifas=removed_ifas, 

2261 new_filters=new_filters, 

2262 user=user, 

2263 ) 

2264 

2265 @transaction.atomic 

2266 def merge_issues(self, issues, user): 

2267 # TODO: This is just a definition of interface, the code is untested 

2268 

2269 # First, add all our current filters to a list 

2270 new_issue_filters = [filter for filter in self.filters.all()] 

2271 

2272 # Collect the list of filters from the issues we want to merge before 

2273 # archiving them 

2274 for issue in issues: 

2275 for filter in issue.filters.all(): 

2276 new_issue_filters.append(filter) 

2277 issue.archive(user) 

2278 

2279 # Set the new list of filters 

2280 self.set_filters(new_issue_filters, user) 

2281 

2282 # Now update our statistics since we possibly re-assigned some new failures 

2283 self.update_statistics() 

2284 

2285 def __str__(self): 

2286 bugs = self.bugs.all() 

2287 if len(bugs) == 0: 

2288 return "Issue: <empty>" 

2289 elif len(bugs) == 1: 

2290 return "Issue: " + str(bugs[0]) 

2291 else: 

2292 return "Issue: [{}]".format(", ".join([b.short_name for b in bugs])) 

2293 

2294 

2295class KnownFailure(models.Model, UserFiltrableMixin): 

2296 # For the FilterMixin. 

2297 filter_objects_to_db = { 

2298 'runconfig': FilterObjectModel(RunConfig, 'result__ts_run__runconfig', 'Run configuration the test is part of'), 

2299 'runconfig_name': FilterObjectStr('result__ts_run__runconfig__name', 'Name of the run configuration'), 

2300 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', 

2301 'Tag associated with the configuration used for this test execution'), 

2302 'runconfig_added_on': FilterObjectDateTime('result__ts_run__runconfig__added_on', 

2303 'Date at which the run configuration got created'), 

2304 'runconfig_temporary': FilterObjectBool('result__ts_run__runconfig__temporary', 

2305 'Is the run configuration temporary, like for pre-merge testing?'), 

2306 'build_name': FilterObjectStr('result__ts_run__runconfig__builds__name', 

2307 'Name of the build for a component used for this test execution'), 

2308 'build_added_on': FilterObjectDateTime('result__ts_run__runconfig__builds__added_on', 

2309 'Date at which the build was added'), 

2310 'component_name': FilterObjectStr('result__ts_run__runconfig__builds__component__name', 

2311 'Name of a component used for this test execution'), 

2312 'machine_name': FilterObjectStr('result__ts_run__machine__name', 'Name of the machine used for this result'), 

2313 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', 

2314 'Tag associated to the machine used in this run'), 

2315 'status_name': FilterObjectStr('result__status__name', 'Name of the resulting status (pass/fail/crash/...)'), 

2316 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

2317 'Name of the testsuite that contains this test'), 

2318 'test_name': FilterObjectStr('result__test__name', 'Name of the test'), 

2319 'test_added_on': FilterObjectDateTime('result__test__added_on', 'Date at which the test got added'), 

2320 'manually_filed_on': FilterObjectDateTime('manually_associated_on', 

2321 'Date at which the failure got manually associated to an issue'), 

2322 'ifa_id': FilterObjectInteger('matched_ifa_id', 

2323 'ID of the associated filter that matched the failure'), 

2324 'issue_id': FilterObjectInteger('matched_ifa__issue_id', 

2325 'ID of the issue associated to the failure'), 

2326 'issue_expected': FilterObjectBool('matched_ifa__issue__expected', 

2327 'Is the issue associated to the failure marked as expected?'), 

2328 'filter_description': FilterObjectStr('matched_ifa__issue__filters__description', 

2329 'Description of what the filter associated to the failure'), 

2330 'filter_runconfig_tag_name': 

2331 FilterObjectStr('matched_ifa__issue__filters__tags__name', 

2332 'Run configuration tag matched by the filter associated to the failure'), 

2333 'filter_machine_tag_name': FilterObjectStr('matched_ifa__issue__filters__machine_tags__name', 

2334 'Machine tag matched by the filter associated to the failure'), 

2335 'filter_machine_name': FilterObjectStr('matched_ifa__issue__filters__machines__name', 

2336 'Name of a machine matched by the filter associated to the failure'), 

2337 'filter_test_name': FilterObjectStr('matched_ifa__issue__filters__tests__name', 

2338 'Name of a test matched by the filter associated to the failure'), 

2339 'filter_status_name': FilterObjectStr('matched_ifa__issue__filters__statuses__name', 

2340 'Status matched by the filter associated to the failure'), 

2341 'filter_stdout_regex': FilterObjectStr('matched_ifa__issue__filters__stdout_regex', 

2342 'Standard output regex used by the filter associated to the failure'), 

2343 'filter_stderr_regex': FilterObjectStr('matched_ifa__issue__filters__stderr_regex', 

2344 'Standard error regex used by the filter associated to the failure'), 

2345 'filter_dmesg_regex': FilterObjectStr('matched_ifa__issue__filters__dmesg_regex', 

2346 'Regex for dmesg used by the filter associated to the failure'), 

2347 'filter_added_on': FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__added_on', 

2348 'Date at which the filter associated to the failure was added on to its issue'), # noqa 

2349 'filter_covers_from': 

2350 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__covers_from', 

2351 'Date of the first failure covered by the filter associated to the failure'), 

2352 'filter_deleted_on': 

2353 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__deleted_on', 

2354 'Date at which the filter was removed from the issue associated to the failure'), 

2355 'filter_runconfigs_covered_count': 

2356 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

2357 'Amount of run configurations covered by the filter associated to the failure'), 

2358 'filter_runconfigs_affected_count': 

2359 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

2360 'Amount of run configurations affected by the filter associated to the failure'), 

2361 'filter_last_seen': 

2362 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__last_seen', 

2363 'Date at which the filter matching this failure was last seen'), 

2364 'filter_last_seen_runconfig_name': 

2365 FilterObjectStr('matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

2366 'Run configuration which last matched the filter associated to the failure'), 

2367 'bug_tracker_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__name', 

2368 'Name of the tracker which holds the bug associated to this failure'), 

2369 'bug_tracker_short_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__short_name', 

2370 'Short name of the tracker which holds the bug associated to this failure'), # noqa 

2371 'bug_tracker_type': FilterObjectStr('matched_ifa__issue__bugs__tracker__tracker_type', 

2372 'Type of the tracker which holds the bug associated to this failure'), 

2373 'bug_id': FilterObjectStr('matched_ifa__issue__bugs__bug_id', 

2374 'ID of the bug associated to this failure'), 

2375 'bug_title': FilterObjectStr('matched_ifa__issue__bugs__title', 

2376 'Title of the bug associated to this failure'), 

2377 'bug_created_on': FilterObjectDateTime('matched_ifa__issue__bugs__created', 

2378 'Date at which the bug associated to this failure was created'), 

2379 'bug_closed_on': FilterObjectDateTime('matched_ifa__issue__bugs__closed', 

2380 'Date at which the bug associated to this failure was closed'), 

2381 'bug_creator_name': FilterObjectStr('matched_ifa__issue__bugs__creator__person__full_name', 

2382 'Name of the creator of the bug associated to this failure'), 

2383 'bug_creator_email': FilterObjectStr('matched_ifa__issue__bugs__creator__person__email', 

2384 'Email address of the creator of the bug associated to this failure'), 

2385 'bug_assignee_name': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__full_name', 

2386 'Name of the assignee of the bug associated to this failure'), 

2387 'bug_assignee_email': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__email', 

2388 'Email address of the assignee of the bug associated to this failure'), 

2389 'bug_product': FilterObjectStr('matched_ifa__issue__bugs__product', 

2390 'Product of the bug associated to this failure'), 

2391 'bug_component': FilterObjectStr('matched_ifa__issue__bugs__component', 

2392 'Component of the bug associated to this failure'), 

2393 'bug_priority': FilterObjectStr('matched_ifa__issue__bugs__priority', 

2394 'Priority of the bug associated to this failure'), 

2395 'bug_features': FilterObjectStr('matched_ifa__issue__bugs__features', 

2396 'Features of the bug associated to this failure (coma-separated list)'), 

2397 'bug_platforms': FilterObjectStr('matched_ifa__issue__bugs__platforms', 

2398 'Platforms of the bug associated to this failure (coma-separated list)'), 

2399 'bug_status': FilterObjectStr('matched_ifa__issue__bugs__status', 

2400 'Status of the bug associated to this failure'), 

2401 'bug_tags': FilterObjectStr('matched_ifa__issue__bugs__tags', 

2402 'Tags/labels on the bug associated to this failure (coma-separated list)'), 

2403 'url': FilterObjectStr('result__url', 'External URL of this test result'), 

2404 'start': FilterObjectDateTime('result__start', 'Date at which this test started being executed'), 

2405 'duration': FilterObjectDuration('result__duration', 'Time it took to execute the test'), 

2406 'command': FilterObjectStr('result__command', 'Command used to execute the test'), 

2407 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'), 

2408 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'), 

2409 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'), 

2410 } 

2411 

2412 result = models.ForeignKey(TestResult, on_delete=models.CASCADE, 

2413 related_name="known_failures", related_query_name="known_failure") 

2414 matched_ifa = models.ForeignKey(IssueFilterAssociated, on_delete=models.CASCADE) 

2415 

2416 # When was the mapping done (useful for metrics) 

2417 manually_associated_on = models.DateTimeField(null=True, blank=True, db_index=True) 

2418 filing_delay = models.DurationField(null=True, blank=True) 

2419 

2420 @classmethod 

2421 def _runconfig_index(cls, covered_list, runconfig): 

2422 try: 

2423 covered = sorted(covered_list, key=lambda r: r.added_on, reverse=True) 

2424 return covered.index(runconfig) 

2425 except ValueError: 

2426 return None 

2427 

2428 @cached_property 

2429 def covered_runconfigs_since_for_issue(self): 

2430 return self._runconfig_index(self.matched_ifa.issue.runconfigs_covered, 

2431 self.result.ts_run.runconfig) 

2432 

2433 @cached_property 

2434 def covered_runconfigs_since_for_filter(self): 

2435 return self._runconfig_index(self.matched_ifa.runconfigs_covered, 

2436 self.result.ts_run.runconfig) 

2437 

2438 def __str__(self): 

2439 return "{} associated on {}".format(str(self.result), self.manually_associated_on) 

2440 

2441 

2442class UnknownFailure(models.Model, UserFiltrableMixin): 

2443 filter_objects_to_db = { 

2444 'test_name': FilterObjectStr('result__test__name', "Name of the test"), 

2445 'status_name': FilterObjectStr('result__status__name', "Name of the status of failure"), 

2446 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

2447 "Name of the testsuite that contains this test"), 

2448 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', "Name of the tag associated to machine"), 

2449 'machine_name': FilterObjectStr('result__ts_run__machine__name', "Name of the associated machine"), 

2450 'runconfig_name': FilterObjectStr('result__ts_run__runconfig__name', "Name of the associated runconfig"), 

2451 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', "Tag associated to runconfig"), 

2452 'bug_title': FilterObjectStr('matched_archived_ifas__issue__bugs__description', 

2453 "Description of bug associated to failure"), 

2454 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'), 

2455 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'), 

2456 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'), 

2457 'build_name': FilterObjectStr('result__test__first_runconfig__builds__name', 'Name of the associated build'), 

2458 } 

2459 # We cannot have two UnknownFailure for the same result 

2460 result = models.OneToOneField(TestResult, on_delete=models.CASCADE, 

2461 related_name="unknown_failure") 

2462 

2463 matched_archived_ifas = models.ManyToManyField(IssueFilterAssociated) 

2464 

2465 @cached_property 

2466 def matched_archived_ifas_cached(self): 

2467 return self.matched_archived_ifas.all() 

2468 

2469 @cached_property 

2470 def matched_issues(self): 

2471 issues = set() 

2472 for e in self.matched_archived_ifas_cached: 

2473 issues.add(e.issue) 

2474 return issues 

2475 

2476 def __str__(self): 

2477 return str(self.result) 

2478 

2479 

2480# Allows us to know if a filter covers/matches a runconfig or not 

2481class RunFilterStatistic(models.Model): 

2482 runconfig = models.ForeignKey(RunConfig, on_delete=models.CASCADE) 

2483 filter = models.ForeignKey(IssueFilter, on_delete=models.CASCADE) 

2484 

2485 covered_count = models.PositiveIntegerField() 

2486 matched_count = models.PositiveIntegerField() 

2487 

2488 class Meta: 

2489 constraints = [ 

2490 UniqueConstraint(fields=('runconfig', 'filter'), name='unique_runconfig_filter') 

2491 ] 

2492 

2493 def __str__(self): 

2494 if self.covered_count > 0: 

2495 perc = self.matched_count * 100 / self.covered_count 

2496 else: 

2497 perc = 0 

2498 return "{} on {}: match rate {}/{} ({:.2f}%)".format(self.filter, 

2499 self.runconfig, 

2500 self.matched_count, 

2501 self.covered_count, 

2502 perc)