Coverage for CIResults / models.py: 95%

1120 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-27 09:21 +0000

1from django.core.validators import RegexValidator, URLValidator 

2from django.core.exceptions import ValidationError 

3from django.conf import settings 

4from django.contrib.auth import get_user_model 

5from django.contrib.auth.models import User 

6from django.contrib.humanize.templatetags.humanize import naturaltime 

7from django.utils.functional import cached_property 

8from django.db import models, transaction 

9from django.utils.timesince import timesince 

10from django.utils import timezone 

11from django.db.models import Q, Exists, OuterRef, UniqueConstraint 

12from django.template.loader import render_to_string 

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

662 def create(self): 

663 try: 

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

665 except ValueError: # pragma: no cover 

666 traceback.print_exc() # pragma: no cover 

667 else: 

668 self.bug_id = id 

669 

670 def __str__(self): 

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

672 

673 

674class BugComment(models.Model, UserFiltrableMixin): 

675 filter_objects_to_db = { 

676 'filter_description': 

677 FilterObjectStr('bug__issue__filters__description', 

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

679 'filter_runconfig_tag_name': 

680 FilterObjectStr('bug__issue__filters__tags__name', 

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

682 'filter_machine_tag_name': 

683 FilterObjectStr('bug__issue__filters__machine_tags__name', 

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

685 'filter_machine_name': 

686 FilterObjectStr('bug__issue__filters__machines__name', 

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

688 'filter_test_name': 

689 FilterObjectStr('bug__issue__filters__tests__name', 

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

691 'filter_status_name': 

692 FilterObjectStr('bug__issue__filters__statuses__name', 

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

694 'filter_stdout_regex': 

695 FilterObjectStr('bug__issue__filters__stdout_regex', 

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

697 'filter_stderr_regex': 

698 FilterObjectStr('bug__issue__filters__stderr_regex', 

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

700 'filter_dmesg_regex': 

701 FilterObjectStr('bug__issue__filters__dmesg_regex', 

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

703 'filter_added_on': 

704 FilterObjectDateTime('bug__issue__issuefilterassociated__added_on', 

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

706 'filter_covers_from': 

707 FilterObjectDateTime('bug__issue__issuefilterassociated__covers_from', 

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

709 'filter_deleted_on': 

710 FilterObjectDateTime('bug__issue__issuefilterassociated__deleted_on', 

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

712 'filter_runconfigs_covered_count': 

713 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_covered_count', 

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

715 'filter_runconfigs_affected_count': 

716 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_affected_count', 

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

718 'filter_last_seen': 

719 FilterObjectDateTime('bug__issue__issuefilterassociated__last_seen', 

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

721 'filter_last_seen_runconfig_name': 

722 FilterObjectStr('bug__issue__issuefilterassociated__last_seen_runconfig__name', 

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

724 

725 'issue_description': FilterObjectStr('bug__issue__description', 

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

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

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

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

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

731 'issue_runconfigs_covered_count': FilterObjectInteger('bug__issue__runconfigs_covered_count', 

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

733 'issue_runconfigs_affected_count': FilterObjectInteger('bug__issue__runconfigs_affected_count', 

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

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

736 'issue_last_seen_runconfig_name': FilterObjectStr('bug__issue__last_seen_runconfig__name', 

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

738 

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

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

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

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

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

744 'bug_created_on': FilterObjectDateTime('bug__created', 

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

746 'bug_updated_on': FilterObjectDateTime('bug__updated', 

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

748 'bug_closed_on': FilterObjectDateTime('bug__closed', 

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

750 'bug_creator_name': FilterObjectStr('bug__creator__person__full_name', 

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

752 'bug_creator_email': FilterObjectStr('bug__creator__person__email', 

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

754 'bug_assignee_name': FilterObjectStr('bug__assignee__person__full_name', 

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

756 'bug_assignee_email': FilterObjectStr('bug__assignee__person__email', 

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

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

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

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

761 'bug_features': FilterObjectStr('bug__features', 

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

763 'bug_platforms': FilterObjectStr('bug__platforms', 

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

765 'bug_status': FilterObjectStr('bug__status', 

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

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

768 

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

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

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

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

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

774 } 

775 

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

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

778 

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

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

781 created_on = models.DateTimeField() 

782 

783 class Meta: 

784 constraints = [ 

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

786 ] 

787 

788 def __str__(self): 

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

790 

791 

792def script_validator(script): 

793 try: 

794 client = Client.get_or_create_instance(script) 

795 except (ValueError, IOError) as e: 

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

797 else: 

798 client.shutdown() 

799 return script 

800 

801 

802class ReplicationScript(models.Model): 

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

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

805 here - :ref:`replication-doc` 

806 """ 

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

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

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

810 created_on = models.DateTimeField(auto_now_add=True, 

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

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

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

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

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

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

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

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

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

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

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

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

823 

824 class Meta: 

825 constraints = [ 

826 UniqueConstraint( 

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

828 name='unique_source_tracker_destination_tracker', 

829 ), 

830 ] 

831 

832 def __str__(self): 

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

834 

835 

836# Software 

837class Component(models.Model): 

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

839 description = models.TextField() 

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

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

842 

843 def __str__(self): 

844 return self.name 

845 

846 

847class Build(models.Model): 

848 # Minimum information needed 

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

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

851 version = models.CharField(max_length=40) 

852 added_on = models.DateTimeField(auto_now=True) 

853 

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

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

856 

857 # Actual build information 

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

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

860 repo = models.CharField( 

861 max_length=200, 

862 null=True, 

863 blank=True, 

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

865 ) 

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

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

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

869 

870 @property 

871 def url(self): 

872 if self.upstream_url is not None: 

873 return self.upstream_url 

874 elif self.repo is not None: 

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

876 else: 

877 return self.version 

878 

879 def __str__(self): 

880 return self.name 

881 

882# Results 

883 

884 

885class VettableObjectMixin: 

886 @property 

887 def vetted(self): 

888 return self.vetted_on is not None 

889 

890 @transaction.atomic 

891 def vet(self): 

892 if self.vetted_on is not None: 

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

894 self.vetted_on = timezone.now() 

895 self.save() 

896 

897 @transaction.atomic 

898 def suppress(self): 

899 if self.vetted_on is None: 

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

901 self.vetted_on = None 

902 self.save() 

903 

904 

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

906 filter_objects_to_db = { 

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

908 'vetted_on': 

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

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

911 'first_runconfig': 

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

913 } 

914 name = models.CharField(max_length=150) 

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

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

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

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

919 added_on = models.DateTimeField(auto_now_add=True) 

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

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

922 

923 class Meta: 

924 ordering = ['name'] 

925 constraints = [ 

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

927 ] 

928 permissions = [ 

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

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

931 ] 

932 

933 def __str__(self): 

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

935 

936 @property 

937 def in_active_ifas(self): 

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

939 

940 @transaction.atomic 

941 def rename(self, new_name): 

942 # Get the matching test, or create it 

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

944 if new_test is None: 

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

946 public=self.public) 

947 else: 

948 new_test.public = self.public 

949 

950 new_test.vetted_on = self.vetted_on 

951 new_test.save() 

952 

953 # Now, update every active IFA 

954 for ifa in self.in_active_ifas: 

955 ifa.filter.tests.add(new_test) 

956 

957 

958class MachineTag(models.Model): 

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

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

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

962 

963 added_on = models.DateTimeField(auto_now_add=True) 

964 

965 class Meta: 

966 ordering = ['name'] 

967 

968 @cached_property 

969 def machines(self): 

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

971 

972 def __str__(self): 

973 return self.name 

974 

975 

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

977 filter_objects_to_db = { 

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

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

980 'vetted_on': 

981 FilterObjectDateTime('vetted_on', 

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

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

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

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

986 } 

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

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

989 

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

991 

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

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

994 

995 added_on = models.DateTimeField(auto_now_add=True) 

996 

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

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

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

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

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

1002 

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

1004 

1005 color_hex = ColoredObjectMixin.color_hex 

1006 

1007 class Meta: 

1008 ordering = ['name'] 

1009 permissions = [ 

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

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

1012 ] 

1013 

1014 @cached_property 

1015 def tags_cached(self): 

1016 return self.tags.all() 

1017 

1018 def __str__(self): 

1019 return self.name 

1020 

1021 

1022class RunConfigTag(models.Model): 

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

1024 help_text="Unique name for the tag") 

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

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

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

1028 

1029 def __str__(self): 

1030 return self.name 

1031 

1032 

1033class RunConfig(models.Model): 

1034 filter_objects_to_db = { 

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

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

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

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

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

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

1041 

1042 # Through reverse accessors 

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

1044 'machine_tag': FilterObjectStr('testsuiterun__machine__tags__name', 

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

1046 } 

1047 

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

1049 tags = models.ManyToManyField(RunConfigTag) 

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

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

1052 

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

1054 

1055 builds = models.ManyToManyField(Build) 

1056 

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

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

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

1060 

1061 @cached_property 

1062 def tags_cached(self): 

1063 return self.tags.all() 

1064 

1065 @cached_property 

1066 def tags_ids_cached(self): 

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

1068 

1069 @cached_property 

1070 def builds_cached(self): 

1071 return self.builds.all() 

1072 

1073 @cached_property 

1074 def builds_ids_cached(self): 

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

1076 

1077 @cached_property 

1078 def public(self): 

1079 for tag in self.tags_cached: 

1080 if not tag.public: 

1081 return False 

1082 return True 

1083 

1084 @cached_property 

1085 def runcfg_history(self): 

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

1087 # the history of this particular run config 

1088 

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

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

1091 tags = self.tags_cached 

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

1093 

1094 @cached_property 

1095 def runcfg_history_offset(self): 

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

1097 if self.id == runcfg.id: 

1098 return i 

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

1100 

1101 def __str__(self): 

1102 return self.name 

1103 

1104 def update_statistics(self): 

1105 stats = [] 

1106 

1107 # Do not compute statistics for temporary runconfigs 

1108 if self.temporary: 

1109 return stats 

1110 

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

1112 

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

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

1115 for filter in filters: 

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

1117 matched_count=0) 

1118 

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

1120 if fs.covered_count < 1: 

1121 continue 

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

1123 fs.matched_count = len(matched_failures) 

1124 stats.append(fs) 

1125 

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

1127 with transaction.atomic(): 

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

1129 RunFilterStatistic.objects.bulk_create(stats) 

1130 

1131 return stats 

1132 

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

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

1135 no_compress=no_compress, query=query) 

1136 

1137 

1138class TestSuite(VettableObjectMixin, Component): 

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

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

1141 

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

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

1144 

1145 # Status to ignore for diffing 

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

1147 related_name='+') 

1148 

1149 class Meta: 

1150 permissions = [ 

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

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

1153 ] 

1154 

1155 @cached_property 

1156 def __acceptable_statuses__(self): 

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

1158 

1159 def __str__(self): 

1160 return self.name 

1161 

1162 def is_failure(self, status): 

1163 return status.id not in self.__acceptable_statuses__ 

1164 

1165 

1166class TestsuiteRun(models.Model, UserFiltrableMixin): 

1167 # For the FilterMixin. 

1168 filter_objects_to_db = { 

1169 'testsuite_name': FilterObjectStr('testsuite__name', 

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

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

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

1173 'runconfig_tag': FilterObjectStr('runconfig__tags__name', 

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

1175 'runconfig_added_on': FilterObjectDateTime('runconfig__added_on', 

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

1177 'runconfig_temporary': FilterObjectBool('runconfig__temporary', 

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

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

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

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

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

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

1184 'reported_on': FilterObjectDateTime('reported_on', 

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

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

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

1188 } 

1189 

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

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

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

1193 run_id = models.IntegerField() 

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

1195 

1196 start = models.DateTimeField() 

1197 duration = models.DurationField() 

1198 reported_on = models.DateTimeField(auto_now_add=True) 

1199 

1200 environment = models.TextField(blank=True, 

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

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

1203 log = models.TextField(blank=True) 

1204 

1205 class Meta: 

1206 constraints = [ 

1207 UniqueConstraint( 

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

1209 name='unique_testsuite_runconfig_machine_run_id', 

1210 ), 

1211 ] 

1212 ordering = ['start'] 

1213 

1214 def __str__(self): 

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

1216 

1217 

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

1219 filter_objects_to_db = { 

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

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

1222 } 

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

1224 name = models.CharField(max_length=20) 

1225 

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

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

1228 added_on = models.DateTimeField(auto_now_add=True) 

1229 

1230 color_hex = ColoredObjectMixin.color_hex 

1231 

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

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

1234 "The best possible is 0.") 

1235 

1236 class Meta: 

1237 constraints = [ 

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

1239 ] 

1240 verbose_name_plural = "Text Statuses" 

1241 permissions = [ 

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

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

1244 ] 

1245 

1246 @property 

1247 def is_failure(self): 

1248 return self.testsuite.is_failure(self) 

1249 

1250 @property 

1251 def is_notrun(self): 

1252 return self == self.testsuite.notrun_status 

1253 

1254 @property 

1255 def actual_severity(self): 

1256 if self.severity is not None: 

1257 return self.severity 

1258 elif self.is_notrun: 

1259 return 0 

1260 elif not self.is_failure: 

1261 return 1 

1262 else: 

1263 return 2 

1264 

1265 def __str__(self): 

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

1267 

1268 

1269class TestResultAssociatedManager(models.Manager): 

1270 def get_queryset(self): 

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

1272 'status', 'ts_run__machine', 

1273 'ts_run__machine__tags', 

1274 'ts_run__runconfig__tags', 

1275 'test') 

1276 

1277 

1278class TestResult(models.Model, UserFiltrableMixin): 

1279 # For the FilterMixin. 

1280 filter_objects_to_db = { 

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

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

1283 'runconfig_tag': FilterObjectStr('ts_run__runconfig__tags__name', 

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

1285 'runconfig_added_on': FilterObjectDateTime('ts_run__runconfig__added_on', 

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

1287 'runconfig_temporary': FilterObjectBool('ts_run__runconfig__temporary', 

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

1289 'build_name': FilterObjectStr('ts_run__runconfig__builds__name', 

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

1291 'build_added_on': FilterObjectDateTime('ts_run__runconfig__builds__added_on', 

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

1293 'component_name': FilterObjectStr('ts_run__runconfig__builds__component__name', 

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

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

1296 'machine_tag': FilterObjectStr('ts_run__machine__tags__name', 

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

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

1299 'testsuite_name': FilterObjectStr('status__testsuite__name', 

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

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

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

1303 'manually_filed_on': FilterObjectDateTime('known_failure__manually_associated_on', 

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

1305 'ifa_id': FilterObjectInteger('known_failure__matched_ifa_id', 

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

1307 'issue_id': FilterObjectInteger('known_failure__matched_ifa__issue_id', 

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

1309 'issue_expected': FilterObjectBool('known_failure__matched_ifa__issue__expected', 

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

1311 'filter_description': FilterObjectStr('known_failure__matched_ifa__issue__filters__description', 

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

1313 'filter_runconfig_tag_name': 

1314 FilterObjectStr('known_failure__matched_ifa__issue__filters__tags__name', 

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

1316 'filter_machine_tag_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machine_tags__name', 

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

1318 'filter_machine_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machines__name', 

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

1320 'filter_test_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__tests__name', 

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

1322 'filter_status_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__statuses__name', 

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

1324 'filter_stdout_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stdout_regex', 

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

1326 'filter_stderr_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stderr_regex', 

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

1328 'filter_dmesg_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__dmesg_regex', 

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

1330 'filter_added_on': FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__added_on', 

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

1332 'filter_covers_from': 

1333 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__covers_from', 

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

1335 'filter_deleted_on': 

1336 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__deleted_on', 

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

1338 'filter_runconfigs_covered_count': 

1339 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

1341 'filter_runconfigs_affected_count': 

1342 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

1344 'filter_last_seen': 

1345 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__last_seen', 

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

1347 'filter_last_seen_runconfig_name': 

1348 FilterObjectStr('known_failure__matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

1350 'bug_tracker_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__name', 

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

1352 'bug_tracker_short_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__short_name', 

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

1354 'bug_tracker_type': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__tracker_type', 

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

1356 'bug_id': FilterObjectStr('known_failure__matched_ifa__issue__bugs__bug_id', 

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

1358 'bug_title': FilterObjectStr('known_failure__matched_ifa__issue__bugs__title', 

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

1360 'bug_created_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__created', 

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

1362 'bug_closed_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__closed', 

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

1364 'bug_creator_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__full_name', 

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

1366 'bug_creator_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__email', 

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

1368 'bug_assignee_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__full_name', 

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

1370 'bug_assignee_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__email', 

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

1372 'bug_product': FilterObjectStr('known_failure__matched_ifa__issue__bugs__product', 

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

1374 'bug_component': FilterObjectStr('known_failure__matched_ifa__issue__bugs__component', 

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

1376 'bug_priority': FilterObjectStr('known_failure__matched_ifa__issue__bugs__priority', 

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

1378 'bug_features': FilterObjectStr('known_failure__matched_ifa__issue__bugs__features', 

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

1380 'bug_platforms': FilterObjectStr('known_failure__matched_ifa__issue__bugs__platforms', 

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

1382 'bug_status': FilterObjectStr('known_failure__matched_ifa__issue__bugs__status', 

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

1384 'bug_tags': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tags', 

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

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

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

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

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

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

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

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

1393 } 

1394 

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

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

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

1398 

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

1400 

1401 start = models.DateTimeField() 

1402 duration = models.DurationField() 

1403 

1404 command = models.CharField(max_length=500) 

1405 stdout = models.TextField(null=True) 

1406 stderr = models.TextField(null=True) 

1407 dmesg = models.TextField(null=True) 

1408 

1409 objects = models.Manager() 

1410 objects_ready_for_matching = TestResultAssociatedManager() 

1411 

1412 @cached_property 

1413 def is_failure(self): 

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

1415 

1416 @cached_property 

1417 def known_failures_cached(self): 

1418 return self.known_failures.all() 

1419 

1420 def __str__(self): 

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

1422 self.test.name, self.status) 

1423 

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

1425 

1426# Issues 

1427 

1428 

1429class IssueFilter(models.Model): 

1430 description = models.CharField(max_length=255, 

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

1432 

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

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

1435 "(leave empty to ignore tags)") 

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

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

1438 "(leave empty to ignore machines)") 

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

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

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

1442 "leave empty to ignore machines)") 

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

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

1445 "(leave empty to ignore tests)") 

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

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

1448 "ignore results)") 

1449 

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

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

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

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

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

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

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

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

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

1459 

1460 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1463 

1464 def delete(self): 

1465 self.hidden = True 

1466 

1467 @cached_property 

1468 def tags_cached(self): 

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

1470 

1471 @cached_property 

1472 def tags_ids_cached(self): 

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

1474 

1475 @cached_property 

1476 def __machines_cached__(self): 

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

1478 

1479 @cached_property 

1480 def __machine_tags_cached__(self): 

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

1482 

1483 @cached_property 

1484 def machines_cached(self): 

1485 machines = self.__machines_cached__.copy() 

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

1487 machines.add(machine) 

1488 return machines 

1489 

1490 @cached_property 

1491 def machines_ids_cached(self): 

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

1493 

1494 @cached_property 

1495 def tests_cached(self): 

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

1497 

1498 @cached_property 

1499 def tests_ids_cached(self): 

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

1501 

1502 @cached_property 

1503 def statuses_cached(self): 

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

1505 

1506 @cached_property 

1507 def statuses_ids_cached(self): 

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

1509 

1510 @cached_property 

1511 def stdout_regex_cached(self): 

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

1513 

1514 @cached_property 

1515 def stderr_regex_cached(self): 

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

1517 

1518 @cached_property 

1519 def dmesg_regex_cached(self): 

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

1521 

1522 @cached_property 

1523 def covered_results(self): 

1524 return QueryParser( 

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

1526 ).objects 

1527 

1528 @cached_property 

1529 def __covers_function(self): 

1530 parser = QueryParserPython( 

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

1532 ) 

1533 if not parser.is_valid: 

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

1535 return parser.matching_fn 

1536 

1537 def covers(self, result): 

1538 try: 

1539 return self.__covers_function(result) 

1540 except ValueError as err: 

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

1542 return False 

1543 

1544 @cached_property 

1545 def matched_results(self): 

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

1547 

1548 @property 

1549 def matched_unknown_failures(self): 

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

1551 

1552 @cached_property 

1553 def __matches_function(self): 

1554 parser = QueryParserPython(TestResult, self.equivalent_user_query) 

1555 if not parser.is_valid: 

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

1557 return parser.matching_fn 

1558 

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

1560 try: 

1561 return self.__matches_function(result) 

1562 except ValueError as err: 

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

1564 return False 

1565 

1566 @transaction.atomic 

1567 def replace(self, new_filter, user): 

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

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

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

1571 

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

1573 self.delete() 

1574 

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

1576 query = [] 

1577 

1578 if covers: 

1579 if len(self.tags_cached) > 0: 

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

1581 

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

1583 if len(self.__machines_cached__) > 0: 

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

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

1586 if len(self.__machine_tags_cached__) > 0: 

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

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

1589 

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

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

1592 elif len(self.__machines_cached__) > 0: 

1593 query.append(machines_query) 

1594 else: 

1595 query.append(machine_tags_query) 

1596 

1597 if len(self.tests_cached) > 0: 

1598 tests_query = [] 

1599 

1600 # group the tests by testsuite 

1601 testsuites = defaultdict(set) 

1602 for test in self.tests_cached: 

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

1604 

1605 # create the sub-queries 

1606 for testsuite in testsuites: 

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

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

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

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

1611 

1612 if matches: 

1613 if len(self.statuses_cached) > 0: 

1614 status_query = [] 

1615 

1616 # group the statuses by testsuite 

1617 testsuites = defaultdict(set) 

1618 for status in self.statuses_cached: 

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

1620 

1621 # create the sub-queries 

1622 for testsuite in testsuites: 

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

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

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

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

1627 

1628 if len(self.stdout_regex) > 0: 

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

1630 

1631 if len(self.stderr_regex) > 0: 

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

1633 

1634 if len(self.dmesg_regex) > 0: 

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

1636 

1637 return " AND ".join(query) 

1638 

1639 @cached_property 

1640 def equivalent_user_query(self) -> str: 

1641 if self.user_query: 

1642 return self.user_query 

1643 return self._to_user_query() 

1644 

1645 def __str__(self): 

1646 return self.description 

1647 

1648 

1649class Rate: 

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

1651 self._type_str = type_str 

1652 self._affected = affected 

1653 self._total = total 

1654 

1655 @property 

1656 def rate(self): 

1657 if self._total > 0: 

1658 return self._affected / self._total 

1659 else: 

1660 return 0 

1661 

1662 def __str__(self): 

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

1664 self._total, 

1665 self._type_str, 

1666 self.rate * 100.0) 

1667 

1668 

1669class IssueFilterAssociatedManager(models.Manager): 

1670 def get_queryset(self): 

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

1672 'filter__machine_tags', 

1673 'filter__machines', 

1674 'filter__tests', 

1675 'filter__statuses', 

1676 'filter') 

1677 

1678 

1679class IssueFilterAssociated(models.Model): 

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

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

1682 

1683 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1686 

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

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

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

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

1691 

1692 # Statistics cache 

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

1694 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1695 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1698 

1699 objects = models.Manager() 

1700 objects_ready_for_matching = IssueFilterAssociatedManager() 

1701 

1702 @property 

1703 def active(self): 

1704 return self.deleted_on is None 

1705 

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

1707 if self.deleted_on is not None: 

1708 return 

1709 

1710 if now is not None: 

1711 self.deleted_on = now 

1712 else: 

1713 self.deleted_on = timezone.now() 

1714 

1715 self.deleted_by = user 

1716 self.save() 

1717 

1718 @cached_property 

1719 def __runfilter_stats_covered__(self): 

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

1721 

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

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

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

1725 # earliest of these two events. 

1726 start_time = self.added_on 

1727 if self.covers_from < start_time: 

1728 start_time = self.covers_from 

1729 

1730 if self.deleted_on is not None: 

1731 return objs.filter(runconfig__added_on__gte=start_time, 

1732 runconfig__added_on__lt=self.deleted_on, 

1733 covered_count__gt=0, 

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

1735 else: 

1736 return objs.filter(runconfig__added_on__gte=start_time, 

1737 covered_count__gt=0, 

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

1739 

1740 @cached_property 

1741 def runconfigs_covered(self): 

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

1743 

1744 @cached_property 

1745 def runconfigs_affected(self): 

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

1747 

1748 @property 

1749 def covered_results(self): 

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

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

1752 

1753 def _add_missing_stats(self): 

1754 # Find the list of runconfig we have stats for 

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

1756 

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

1758 stats = dict() 

1759 results = ( 

1760 self.covered_results.exclude(ts_run__runconfig__id__in=runconfigs_done) 

1761 .filter(ts_run__runconfig__temporary=False) 

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

1763 ) 

1764 for result in results: 

1765 runconfig = result.ts_run.runconfig 

1766 fs = stats.get(runconfig) 

1767 if fs is None: 

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

1769 matched_count=0, covered_count=0) 

1770 

1771 fs.covered_count += 1 

1772 

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

1774 # query to also check if they matched. 

1775 # 

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

1777 # list of ids we got previously 

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

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

1780 for result in query: 

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

1782 

1783 # Save the statistics objects 

1784 for fs in stats.values(): 

1785 fs.save() 

1786 

1787 def update_statistics(self): 

1788 # drop all the caches 

1789 try: 

1790 del self.__runfilter_stats_covered__ 

1791 del self.runconfigs_covered 

1792 del self.runconfigs_affected 

1793 except AttributeError: 

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

1795 pass 

1796 

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

1798 req = req.order_by("result__ts_run__runconfig__added_on") 

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

1800 if oldest_failure is not None: 

1801 self.covers_from = oldest_failure 

1802 

1803 # get the list of runconfigs needing update 

1804 self._add_missing_stats() 

1805 

1806 self.runconfigs_covered_count = len(self.runconfigs_covered) 

1807 self.runconfigs_affected_count = len(self.runconfigs_affected) 

1808 

1809 # Find when the issue was last seen 

1810 for stats in self.__runfilter_stats_covered__: 

1811 if stats.matched_count > 0: 

1812 self.last_seen = stats.runconfig.added_on 

1813 self.last_seen_runconfig = stats.runconfig 

1814 break 

1815 

1816 # Update the statistics atomically in the DB 

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

1818 cur_ifa.update(covers_from=self.covers_from, 

1819 runconfigs_covered_count=self.runconfigs_covered_count, 

1820 runconfigs_affected_count=self.runconfigs_affected_count, 

1821 last_seen=self.last_seen, 

1822 last_seen_runconfig=self.last_seen_runconfig) 

1823 

1824 @property 

1825 def failure_rate(self): 

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

1827 

1828 @property 

1829 def activity_period(self): 

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

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

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

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

1834 

1835 if not self.active: 

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

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

1838 naturaltime(self.deleted_on), deleted_by, 

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

1840 else: 

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

1842 

1843 def __str__(self): 

1844 if self.deleted_on is not None: 

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

1846 else: 

1847 delete_on = "" 

1848 

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

1850 

1851 

1852class Issue(models.Model, UserFiltrableMixin): 

1853 filter_objects_to_db = { 

1854 'filter_description': FilterObjectStr('filters__description', 

1855 'Description of what the filter matches'), 

1856 'filter_runconfig_tag_name': FilterObjectStr('filters__tags__name', 

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

1858 'filter_machine_tag_name': FilterObjectStr('filters__machine_tags__name', 

1859 'Machine tag matched by the filter'), 

1860 'filter_machine_name': FilterObjectStr('filters__machines__name', 

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

1862 'filter_test_name': FilterObjectStr('filters__tests__name', 

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

1864 'filter_status_name': FilterObjectStr('filters__statuses__name', 

1865 'Status matched by the filter'), 

1866 'filter_stdout_regex': FilterObjectStr('filters__stdout_regex', 

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

1868 'filter_stderr_regex': FilterObjectStr('filters__stderr_regex', 

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

1870 'filter_dmesg_regex': FilterObjectStr('filters__dmesg_regex', 

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

1872 'filter_added_on': FilterObjectDateTime('issuefilterassociated__added_on', 

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

1874 'filter_covers_from': FilterObjectDateTime('issuefilterassociated__covers_from', 

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

1876 'filter_deleted_on': FilterObjectDateTime('issuefilterassociated__deleted_on', 

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

1878 'filter_runconfigs_covered_count': FilterObjectInteger('issuefilterassociated__runconfigs_covered_count', 

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

1880 'filter_runconfigs_affected_count': FilterObjectInteger('issuefilterassociated__runconfigs_affected_count', 

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

1882 'filter_last_seen': FilterObjectDateTime('issuefilterassociated__last_seen', 

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

1884 'filter_last_seen_runconfig_name': FilterObjectStr('issuefilterassociated__last_seen_runconfig__name', 

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

1886 

1887 'bug_tracker_name': FilterObjectStr('bugs__tracker__name', 

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

1889 'bug_tracker_short_name': FilterObjectStr('bugs__tracker__short_name', 

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

1891 'bug_tracker_type': FilterObjectStr('bugs__tracker__tracker_type', 

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

1893 'bug_id': FilterObjectStr('bugs__bug_id', 

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

1895 'bug_title': FilterObjectStr('bugs__title', 

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

1897 'bug_created_on': FilterObjectDateTime('bugs__created', 

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

1899 'bug_updated_on': FilterObjectDateTime('bugs__updated', 

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

1901 'bug_closed_on': FilterObjectDateTime('bugs__closed', 

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

1903 'bug_creator_name': FilterObjectStr('bugs__creator__person__full_name', 

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

1905 'bug_creator_email': FilterObjectStr('bugs__creator__person__email', 

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

1907 'bug_assignee_name': FilterObjectStr('bugs__assignee__person__full_name', 

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

1909 'bug_assignee_email': FilterObjectStr('bugs__assignee__person__email', 

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

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

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

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

1914 'bug_features': FilterObjectStr('bugs__features', 

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

1916 'bug_platforms': FilterObjectStr('bugs__platforms', 

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

1918 'bug_status': FilterObjectStr('bugs__status', 

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

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

1921 'bug_tags': FilterObjectStr('bugs__tags', 

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

1923 

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

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

1926 

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

1928 

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

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

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

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

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

1934 'runconfigs_covered_count': FilterObjectInteger('runconfigs_covered_count', 

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

1936 'runconfigs_affected_count': FilterObjectInteger('runconfigs_affected_count', 

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

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

1939 'last_seen_runconfig_name': FilterObjectStr('last_seen_runconfig__name', 

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

1941 } 

1942 

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

1944 bugs = models.ManyToManyField(Bug) 

1945 

1946 description = models.TextField(blank=True) 

1947 filer = models.EmailField() # DEPRECATED 

1948 

1949 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1952 

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

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

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

1956 

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

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

1959 

1960 # Statistics cache 

1961 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1962 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1965 

1966 class Meta: 

1967 permissions = [ 

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

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

1970 

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

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

1973 ] 

1974 

1975 @property 

1976 def archived(self): 

1977 return self.archived_on is not None 

1978 

1979 def hide(self): 

1980 self.expected = True 

1981 self.save() 

1982 

1983 def show(self): 

1984 self.expected = False 

1985 self.save() 

1986 

1987 @cached_property 

1988 def active_filters(self): 

1989 if self.archived: 

1990 deleted_on = self.archived_on 

1991 else: 

1992 deleted_on = None 

1993 

1994 if hasattr(self, 'ifas_cached'): 

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

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

1997 else: 

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

1999 deleted_on=deleted_on) 

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

2001 

2002 @cached_property 

2003 def all_filters(self): 

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

2005 

2006 @property 

2007 def past_filters(self): 

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

2009 

2010 @cached_property 

2011 def bugs_cached(self): 

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

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

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

2015 

2016 @cached_property 

2017 def covers_from(self): 

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

2019 

2020 @cached_property 

2021 def __runfilter_stats_covered__(self): 

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

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

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

2025 covered_count__gt=0, 

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

2027 if self.archived: 

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

2029 

2030 return objs 

2031 

2032 @cached_property 

2033 def runconfigs_covered(self): 

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

2035 

2036 @cached_property 

2037 def runconfigs_affected(self): 

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

2039 # to the set of affected ones 

2040 runconfigs_affected = set() 

2041 for runfilter in self.__runfilter_stats_covered__: 

2042 if runfilter.matched_count > 0: 

2043 runconfigs_affected.add(runfilter.runconfig) 

2044 

2045 return runconfigs_affected 

2046 

2047 def update_statistics(self): 

2048 self.runconfigs_covered_count = len(self.runconfigs_covered) 

2049 self.runconfigs_affected_count = len(self.runconfigs_affected) 

2050 

2051 # Find when the issue was last seen 

2052 for stats in self.__runfilter_stats_covered__: 

2053 if stats.matched_count > 0: 

2054 self.last_seen = stats.runconfig.added_on 

2055 self.last_seen_runconfig = stats.runconfig 

2056 break 

2057 

2058 # Update the statistics atomically in the DB 

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

2060 cur_issue.update(runconfigs_covered_count=self.runconfigs_covered_count, 

2061 runconfigs_affected_count=self.runconfigs_affected_count, 

2062 last_seen=self.last_seen, 

2063 last_seen_runconfig=self.last_seen_runconfig) 

2064 

2065 @property 

2066 def failure_rate(self): 

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

2068 

2069 def matches(self, result): 

2070 if self.archived: 

2071 return False 

2072 

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

2074 if e.filter.matches(result): 

2075 return True 

2076 return False 

2077 

2078 def archive(self, user): 

2079 if self.archived: 

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

2081 

2082 with transaction.atomic(): 

2083 now = timezone.now() 

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

2085 e.delete(user, now) 

2086 self.archived_on = now 

2087 self.archived_by = user 

2088 self.save() 

2089 

2090 # Post a comment 

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

2092 

2093 def restore(self): 

2094 if not self.archived: 

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

2096 

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

2098 with transaction.atomic(): 

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

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

2101 

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

2103 self.archived_on = None 

2104 self.archived_by = None 

2105 self.save() 

2106 

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

2108 self.update_statistics() 

2109 

2110 # Post a comment 

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

2112 

2113 @transaction.atomic 

2114 def set_bugs(self, bugs): 

2115 if self.archived: 

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

2117 

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

2119 self.bugs.clear() 

2120 

2121 for bug in bugs: 

2122 # Add first runconfig the bug was seen in 

2123 bug.add_first_seen_in(self) 

2124 

2125 # Make sure the bug exists in the database 

2126 if bug.id is None: 

2127 bug.save() 

2128 

2129 # Add it to the relation 

2130 self.bugs.add(bug) 

2131 

2132 # Get rid of the cached bugs 

2133 try: 

2134 del self.bugs_cached 

2135 except AttributeError: 

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

2137 pass 

2138 

2139 def _assign_to_known_failures(self, unknown_failures, ifa): 

2140 now = timezone.now() 

2141 new_matched_failures = [] 

2142 for failure in unknown_failures: 

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

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

2145 manually_associated_on=now, 

2146 filing_delay=filing_delay) 

2147 new_matched_failures.append(kf) 

2148 failure.delete() 

2149 

2150 ifa.update_statistics() 

2151 

2152 return new_matched_failures 

2153 

2154 def __filter_add__(self, filter, user): 

2155 # Make sure the filter exists in the database first 

2156 if filter.id is None: 

2157 filter.save() 

2158 

2159 # Create the association between the filter and the issue 

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

2161 

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

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

2164 matched_unknown_failures = ( 

2165 filter.matched_unknown_failures.select_related("result") 

2166 .prefetch_related("result__ts_run") 

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

2168 ) 

2169 return self._assign_to_known_failures(matched_unknown_failures, ifa) 

2170 

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

2172 basic_comment: str = render_to_string(template, template_kwargs) 

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

2174 

2175 try: 

2176 for bug in self.bugs_cached: 

2177 if is_new and bug.first_seen_in: 

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

2179 comment: str = render_to_string(template, template_kwargs) 

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

2181 else: 

2182 comment = basic_comment 

2183 bug.add_comment(comment) 

2184 except Exception: # pragma: no cover 

2185 traceback.print_exc() # pragma: no cover 

2186 

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

2188 if self.archived: 

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

2190 

2191 with transaction.atomic(): 

2192 # First, add the new filter 

2193 failures = self.__filter_add__(new_filter, user) 

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

2195 

2196 # Delete all active associations of the old filter 

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

2198 for e in assocs: 

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

2200 

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

2202 self.update_statistics() 

2203 

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

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

2206 old_filter.equivalent_user_query != new_filter.equivalent_user_query or 

2207 len(new_matched_failures) > 0): 

2208 self.render_and_leave_comment_on_all_bugs( 

2209 "CIResults/issue_replace_filter_comment.txt", 

2210 issue=self, 

2211 old_filter=old_filter, 

2212 new_filter=new_filter, 

2213 new_matched_failures=new_matched_failures, 

2214 user=user, 

2215 ) 

2216 

2217 def set_filters(self, filters, user): 

2218 if self.archived: 

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

2220 

2221 with transaction.atomic(): 

2222 removed_ifas = set() 

2223 new_filters = dict() 

2224 

2225 # Query the set of issues that we currently have 

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

2227 

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

2229 now = timezone.now() 

2230 for e in assocs: 

2231 if e.filter not in filters: 

2232 e.delete(user, now) 

2233 removed_ifas.add(e) 

2234 

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

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

2237 for filter in filters: 

2238 if filter.id not in cur_filters_ids: 

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

2240 

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

2242 self.update_statistics() 

2243 

2244 # Get rid of the cached filters 

2245 try: 

2246 del self.active_filters 

2247 except AttributeError: 

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

2249 pass 

2250 

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

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

2253 self.render_and_leave_comment_on_all_bugs( 

2254 "CIResults/issue_set_filters_comment.txt", 

2255 True, 

2256 issue=self, 

2257 removed_ifas=removed_ifas, 

2258 new_filters=new_filters, 

2259 user=user, 

2260 ) 

2261 

2262 @transaction.atomic 

2263 def merge_issues(self, issues, user): 

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

2265 

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

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

2268 

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

2270 # archiving them 

2271 for issue in issues: 

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

2273 new_issue_filters.append(filter) 

2274 issue.archive(user) 

2275 

2276 # Set the new list of filters 

2277 self.set_filters(new_issue_filters, user) 

2278 

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

2280 self.update_statistics() 

2281 

2282 def __str__(self): 

2283 bugs = self.bugs.all() 

2284 if len(bugs) == 0: 

2285 return "Issue: <empty>" 

2286 elif len(bugs) == 1: 

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

2288 else: 

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

2290 

2291 

2292class KnownFailure(models.Model, UserFiltrableMixin): 

2293 # For the FilterMixin. 

2294 filter_objects_to_db = { 

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

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

2297 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', 

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

2299 'runconfig_added_on': FilterObjectDateTime('result__ts_run__runconfig__added_on', 

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

2301 'runconfig_temporary': FilterObjectBool('result__ts_run__runconfig__temporary', 

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

2303 'build_name': FilterObjectStr('result__ts_run__runconfig__builds__name', 

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

2305 'build_added_on': FilterObjectDateTime('result__ts_run__runconfig__builds__added_on', 

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

2307 'component_name': FilterObjectStr('result__ts_run__runconfig__builds__component__name', 

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

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

2310 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', 

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

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

2313 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

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

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

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

2317 'manually_filed_on': FilterObjectDateTime('manually_associated_on', 

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

2319 'ifa_id': FilterObjectInteger('matched_ifa_id', 

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

2321 'issue_id': FilterObjectInteger('matched_ifa__issue_id', 

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

2323 'issue_expected': FilterObjectBool('matched_ifa__issue__expected', 

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

2325 'filter_description': FilterObjectStr('matched_ifa__issue__filters__description', 

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

2327 'filter_runconfig_tag_name': 

2328 FilterObjectStr('matched_ifa__issue__filters__tags__name', 

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

2330 'filter_machine_tag_name': FilterObjectStr('matched_ifa__issue__filters__machine_tags__name', 

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

2332 'filter_machine_name': FilterObjectStr('matched_ifa__issue__filters__machines__name', 

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

2334 'filter_test_name': FilterObjectStr('matched_ifa__issue__filters__tests__name', 

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

2336 'filter_status_name': FilterObjectStr('matched_ifa__issue__filters__statuses__name', 

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

2338 'filter_stdout_regex': FilterObjectStr('matched_ifa__issue__filters__stdout_regex', 

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

2340 'filter_stderr_regex': FilterObjectStr('matched_ifa__issue__filters__stderr_regex', 

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

2342 'filter_dmesg_regex': FilterObjectStr('matched_ifa__issue__filters__dmesg_regex', 

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

2344 'filter_added_on': FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__added_on', 

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

2346 'filter_covers_from': 

2347 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__covers_from', 

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

2349 'filter_deleted_on': 

2350 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__deleted_on', 

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

2352 'filter_runconfigs_covered_count': 

2353 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

2355 'filter_runconfigs_affected_count': 

2356 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

2358 'filter_last_seen': 

2359 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__last_seen', 

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

2361 'filter_last_seen_runconfig_name': 

2362 FilterObjectStr('matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

2364 'bug_tracker_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__name', 

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

2366 'bug_tracker_short_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__short_name', 

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

2368 'bug_tracker_type': FilterObjectStr('matched_ifa__issue__bugs__tracker__tracker_type', 

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

2370 'bug_id': FilterObjectStr('matched_ifa__issue__bugs__bug_id', 

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

2372 'bug_title': FilterObjectStr('matched_ifa__issue__bugs__title', 

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

2374 'bug_created_on': FilterObjectDateTime('matched_ifa__issue__bugs__created', 

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

2376 'bug_closed_on': FilterObjectDateTime('matched_ifa__issue__bugs__closed', 

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

2378 'bug_creator_name': FilterObjectStr('matched_ifa__issue__bugs__creator__person__full_name', 

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

2380 'bug_creator_email': FilterObjectStr('matched_ifa__issue__bugs__creator__person__email', 

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

2382 'bug_assignee_name': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__full_name', 

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

2384 'bug_assignee_email': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__email', 

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

2386 'bug_product': FilterObjectStr('matched_ifa__issue__bugs__product', 

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

2388 'bug_component': FilterObjectStr('matched_ifa__issue__bugs__component', 

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

2390 'bug_priority': FilterObjectStr('matched_ifa__issue__bugs__priority', 

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

2392 'bug_features': FilterObjectStr('matched_ifa__issue__bugs__features', 

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

2394 'bug_platforms': FilterObjectStr('matched_ifa__issue__bugs__platforms', 

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

2396 'bug_status': FilterObjectStr('matched_ifa__issue__bugs__status', 

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

2398 'bug_tags': FilterObjectStr('matched_ifa__issue__bugs__tags', 

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

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

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

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

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

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

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

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

2407 } 

2408 

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

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

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

2412 

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

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

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

2416 

2417 @classmethod 

2418 def _runconfig_index(cls, covered_list, runconfig): 

2419 try: 

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

2421 return covered.index(runconfig) 

2422 except ValueError: 

2423 return None 

2424 

2425 @cached_property 

2426 def covered_runconfigs_since_for_issue(self): 

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

2428 self.result.ts_run.runconfig) 

2429 

2430 @cached_property 

2431 def covered_runconfigs_since_for_filter(self): 

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

2433 self.result.ts_run.runconfig) 

2434 

2435 def __str__(self): 

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

2437 

2438 

2439class UnknownFailure(models.Model, UserFiltrableMixin): 

2440 filter_objects_to_db = { 

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

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

2443 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

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

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

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

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

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

2449 'bug_title': FilterObjectStr('matched_archived_ifas__issue__bugs__description', 

2450 "Description of bug associated to failure"), 

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

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

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

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

2455 } 

2456 # We cannot have two UnknownFailure for the same result 

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

2458 related_name="unknown_failure") 

2459 

2460 matched_archived_ifas = models.ManyToManyField(IssueFilterAssociated) 

2461 

2462 @cached_property 

2463 def matched_archived_ifas_cached(self): 

2464 return self.matched_archived_ifas.all() 

2465 

2466 @cached_property 

2467 def matched_issues(self): 

2468 issues = set() 

2469 for e in self.matched_archived_ifas_cached: 

2470 issues.add(e.issue) 

2471 return issues 

2472 

2473 def __str__(self): 

2474 return str(self.result) 

2475 

2476 

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

2478class RunFilterStatistic(models.Model): 

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

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

2481 

2482 covered_count = models.PositiveIntegerField() 

2483 matched_count = models.PositiveIntegerField() 

2484 

2485 class Meta: 

2486 constraints = [ 

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

2488 ] 

2489 

2490 def __str__(self): 

2491 if self.covered_count > 0: 

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

2493 else: 

2494 perc = 0 

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

2496 self.runconfig, 

2497 self.matched_count, 

2498 self.covered_count, 

2499 perc)