Coverage for CIResults/models.py: 96%

1121 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-23 13:11 +0000

1from django.core.validators import RegexValidator, URLValidator 

2from django.core.exceptions import ValidationError 

3from django.conf import settings 

4from django.contrib.auth import get_user_model 

5from django.contrib.auth.models import User 

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

7from django.utils.functional import cached_property 

8from django.db import models, transaction 

9from django.utils.timesince import timesince 

10from django.utils import timezone 

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

12from django.template.loader import render_to_string 

13 

14from .runconfigdiff import RunConfigDiff 

15from .filtering import QueryParser, UserFiltrableMixin, FilterObjectStr, FilterObjectDateTime 

16from .filtering import FilterObjectDuration, FilterObjectBool, FilterObjectInteger 

17from .filtering import FilterObjectModel, FilterObjectJSON 

18from .sandbox.io import Client 

19 

20from collections import defaultdict, OrderedDict 

21 

22from datetime import timedelta 

23import hashlib 

24import traceback 

25import re 

26 

27 

28def get_sentinel_user(): 

29 return get_user_model().objects.get_or_create(username='<deleted>', 

30 defaults={'email': 'deleted.user@admin', 

31 'last_login': timezone.now()})[0].id 

32 

33 

34class ColoredObjectMixin: 

35 color_hex = models.CharField(max_length=7, null=True, blank=True, 

36 help_text="Color that should be used to represent this object, in hex format. " 

37 "Use https://www.w3schools.com/colors/colors_picker.asp to pick a color.", 

38 validators=[RegexValidator( 

39 regex='^#[0-9a-f]{6}$', 

40 message='Not a valid hex color format (example: #89ab13)', 

41 )]) 

42 

43 @cached_property 

44 def color(self): 

45 if self.color_hex is not None: 

46 return self.color_hex 

47 

48 # Generate a random color 

49 blake2 = hashlib.blake2b() 

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

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

52 

53 

54# Bug tracking 

55 

56 

57class BugTrackerSLA(models.Model): 

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

59 priority = models.CharField(max_length=50, help_text="Name of the priority you want to define the " 

60 "SLA for (case insensitive)") 

61 SLA = models.DurationField(help_text="Expected SLA for this priority") 

62 

63 class Meta: 

64 constraints = [ 

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

66 ] 

67 verbose_name_plural = "Bug Tracker SLAs" 

68 

69 def __str__(self): 

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

71 

72 

73class Person(models.Model): 

74 full_name = models.CharField(max_length=100, help_text="Full name of the person", blank=True, null=True) 

75 email = models.EmailField(null=True) 

76 

77 added_on = models.DateTimeField(auto_now_add=True) 

78 

79 def __str__(self): 

80 has_full_name = self.full_name is not None and len(self.full_name) > 0 

81 has_email = self.email is not None and len(self.email) > 0 

82 

83 if has_full_name and has_email: 

84 return "{} <{}>".format(self.full_name, self.email) 

85 elif has_full_name: 

86 return self.full_name 

87 elif has_email: 

88 return self.email 

89 else: 

90 return "(No name or email)" 

91 

92 

93class BugTrackerAccount(models.Model): 

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

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

96 

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

98 

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

100 

101 class Meta: 

102 constraints = [ 

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

104 ] 

105 

106 def __str__(self): 

107 return str(self.person) 

108 

109 

110class BugTracker(models.Model): 

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

112 

113 name = models.CharField(max_length=50, unique=True, help_text="Full name of the bugtracker (e.g. Freedesktop.org)") 

114 short_name = models.CharField(max_length=10, help_text="Very shorthand name (e.g. fdo)") 

115 project = models.CharField(max_length=50, blank=True, null=True, 

116 help_text="Specific project key in bugtracker to use (e.g. " 

117 "gitlab (project id): 1234 " 

118 "bugzilla (product/component): Mesa/EGL" 

119 "jira (project key): TEST)") 

120 separator = models.CharField(max_length=1, 

121 help_text="Separator to construct a shorthand of the bug (e.g. '#' to get fdo#1234)") 

122 public = models.BooleanField(help_text="Should bugs filed on this tracker be visible on the public website?") 

123 

124 url = models.URLField(help_text="Public URL to the bugtracker (e.g. " 

125 "gitlab: https://gitlab.freedesktop.org " 

126 "bugzilla: https://bugs.freedesktop.org)") 

127 

128 bug_base_url = models.URLField(help_text="Base URL for constructing (e.g. " 

129 "gitlab: https://gitlab.freedesktop.org/cibuglog/cibuglog/issues/ " 

130 "bugzilla: https://bugs.freedesktop.org/show_bug.cgi?id=)") 

131 

132 tracker_type = models.CharField(max_length=50, help_text="Legal values: bugzilla, gitlab, jira, untracked") 

133 username = models.CharField(max_length=50, blank=True, 

134 help_text="Username to connect to the bugtracker. " 

135 "Leave empty if the bugs are public or if you do not to post comments.") 

136 password = models.CharField(max_length=50, blank=True, 

137 help_text="Password to connect to the bugtracker. " 

138 "Leave empty if the bugs are public or if you do not to post comments.") 

139 # Stats 

140 polled = models.DateTimeField(null=True, blank=True, 

141 help_text="Last time the bugtracker was polled. To be filled automatically.") 

142 

143 # Configurations 

144 components_followed = models.TextField(null=True, blank=True, 

145 help_text="Coma-separated list of components you would like to " 

146 "keep track of. On Gitlab, specify the list of labels an issue " 

147 "needs to have leave empty if you want all issues.") 

148 components_followed_since = models.DateTimeField(blank=True, null=True, 

149 help_text="Poll bugs older than this date. WARNING: " 

150 "Run poll_all_bugs.py after changing this date.") 

151 first_response_SLA = models.DurationField(help_text="Time given to developers to provide the first " 

152 "response after its creation", default=timedelta(days=1)) 

153 custom_fields_map = models.JSONField(null=True, blank=True, 

154 help_text="Mapping of custom_fields that should be included when polling Bugs " 

155 "from this BugTracker, e.g. " 

156 "{'customfield_xxxx': 'severity', 'customfield_yyyy': 'build_nro'}. " 

157 "If a customfield mapping corresponds to an existing Bug field, " 

158 "e.g. severity, the corresponding Bug field will be populated. " 

159 "If not, e.g. build_nro, this will be populated in the Bug's " 

160 "custom_fields field. (Leave empty if not using custom fields)") 

161 

162 @cached_property 

163 def SLAs_cached(self): 

164 slas = dict() 

165 for sla_entry in BugTrackerSLA.objects.filter(tracker=self): 

166 slas[sla_entry.priority.lower()] = sla_entry.SLA 

167 return slas 

168 

169 @cached_property 

170 def tracker(self): 

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

172 

173 if self.tracker_type == "bugzilla": 

174 return Bugzilla(self) 

175 elif self.tracker_type == "gitlab": 

176 return GitLab(self) 

177 elif self.tracker_type == "jira": 

178 return Jira(self) 

179 elif self.tracker_type == "jira_untracked" or self.tracker_type == "untracked": 

180 return Untracked(self) 

181 else: 

182 raise ValueError("The bugtracker type '{}' is unknown".format(self.tracker_type)) 

183 

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

185 self.tracker.poll(bug, force_polling_comments) 

186 bug.polled = self.tracker_time 

187 

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

189 if bugs is None: 

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

191 

192 for bug in bugs: 

193 # Make sure the event object did not signal us to stop 

194 if stop_event is not None and stop_event.is_set(): 

195 return 

196 

197 try: 

198 self.poll(bug) 

199 except Exception: # pragma: no cover 

200 print("{} could not be polled:".format(bug)) # pragma: no cover 

201 traceback.print_exc() # pragma: no cover 

202 

203 bug.save() 

204 

205 # We do not catch any exceptions, so if we reach this point, it means 

206 # all bugs have been updated. 

207 self.polled = self.tracker_time 

208 self.save() 

209 

210 @property 

211 def tracker_time(self): 

212 return self.tracker._get_tracker_time() 

213 

214 def to_tracker_tz(self, dt): 

215 return self.tracker._to_tracker_tz(dt) 

216 

217 @property 

218 def open_statuses(self): 

219 return self.tracker.open_statuses 

220 

221 def is_bug_open(self, bug): 

222 return bug.status in self.open_statuses 

223 

224 @property 

225 def components_followed_list(self): 

226 if self.components_followed is None: 

227 return [] 

228 else: 

229 return [c.strip() for c in self.components_followed.split(',')] 

230 

231 @transaction.atomic 

232 def get_or_create_bugs(self, ids): 

233 new_bugs = set() 

234 

235 known_bugs = Bug.objects.filter(tracker=self, bug_id__in=ids) 

236 known_bug_ids = set([b.bug_id for b in known_bugs]) 

237 for bug_id in ids - known_bug_ids: 

238 new_bugs.add(Bug.objects.create(tracker=self, bug_id=bug_id)) 

239 

240 return set(known_bugs).union(new_bugs) 

241 

242 def __set_tracker_to_bugs__(self, bugs): 

243 for bug in bugs: 

244 bug.tracker = self 

245 return bugs 

246 

247 # WARNING: Some bugs may not have been polled yet 

248 def open_bugs(self): 

249 open_bugs = set(Bug.objects.filter(tracker=self, status__in=self.open_statuses)) 

250 

251 if not self.tracker.has_components or len(self.components_followed_list) > 0: 

252 open_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list, 

253 status=self.open_statuses) 

254 open_bugs.update(self.get_or_create_bugs(open_bug_ids)) 

255 

256 return self.__set_tracker_to_bugs__(open_bugs) 

257 

258 def bugs_in_issues(self): 

259 # Get the list of bugs from this tracker associated to active issues 

260 bugs_in_issues = set() 

261 for issue in Issue.objects.filter(archived_on=None).prefetch_related('bugs__tracker'): 

262 for bug in issue.bugs.filter(tracker=self): 

263 bugs_in_issues.add(bug) 

264 

265 return bugs_in_issues 

266 

267 def followed_bugs(self): 

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

269 

270 def updated_bugs(self): 

271 if not self.polled: 

272 return self.followed_bugs() 

273 

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

275 

276 td = self.tracker_time - timezone.now() 

277 polled_time = self.to_tracker_tz(self.polled + td) 

278 all_upd_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list, 

279 updated_since=polled_time) 

280 not_upd_ids = all_bug_ids - all_upd_bug_ids 

281 

282 open_bug_ids = set() 

283 if not self.tracker.has_components or len(self.components_followed_list) > 0: 

284 open_bug_ids = self.tracker.search_bugs_ids(components=self.components_followed_list, 

285 status=self.open_statuses) 

286 

287 open_db_bug_ids = set(Bug.objects.filter(tracker=self, 

288 status__in=self.open_statuses).values_list('bug_id', flat=True)) 

289 issue_bugs_ids = set(map(lambda x: x.bug_id, self.bugs_in_issues())) 

290 

291 bug_ids = (open_bug_ids | open_db_bug_ids | issue_bugs_ids) - not_upd_ids 

292 upd_bugs = self.get_or_create_bugs(bug_ids) 

293 

294 return self.__set_tracker_to_bugs__(upd_bugs) 

295 

296 def unreplicated_bugs(self): 

297 unrep_bug_ids = set(Bug.objects.filter(~Exists(Bug.objects.filter(parent=OuterRef('pk'))), 

298 parent__isnull=True, 

299 tracker=self, 

300 status__in=self.open_statuses).values_list('bug_id', flat=True)) 

301 

302 unrep_bugs = self.get_or_create_bugs(unrep_bug_ids) 

303 

304 return self.__set_tracker_to_bugs__(unrep_bugs) 

305 

306 def clean(self): 

307 if self.custom_fields_map is None: 

308 return 

309 

310 for field in self.custom_fields_map: 

311 if self.custom_fields_map[field] is None: 

312 continue 

313 self.custom_fields_map[field] = str(self.custom_fields_map[field]) 

314 

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

316 self.clean() 

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

318 

319 def __str__(self): 

320 return self.name 

321 

322 

323class Bug(models.Model, UserFiltrableMixin): 

324 filter_objects_to_db = { 

325 'filter_description': 

326 FilterObjectStr('issue__filters__description', 

327 'Description of what the filter associated to an issue referencing this bug matches'), 

328 'filter_runconfig_tag_name': 

329 FilterObjectStr('issue__filters__tags__name', 

330 'Run configuration tag matched by the filter associated to an issue referencing this bug'), 

331 'filter_machine_tag_name': 

332 FilterObjectStr('issue__filters__machine_tags__name', 

333 'Machine tag matched by the filter associated to an issue referencing this bug'), 

334 'filter_machine_name': 

335 FilterObjectStr('issue__filters__machines__name', 

336 'Name of a machine matched by the filter associated to an issue referencing this bug'), 

337 'filter_test_name': 

338 FilterObjectStr('issue__filters__tests__name', 

339 'Name of a test matched by the filter associated to an issue referencing this bug'), 

340 'filter_status_name': 

341 FilterObjectStr('issue__filters__statuses__name', 

342 'Status matched by the filter associated to an issue referencing this bug'), 

343 'filter_stdout_regex': 

344 FilterObjectStr('issue__filters__stdout_regex', 

345 'Standard output regex used by the filter associated to an issue referencing this bug'), 

346 'filter_stderr_regex': 

347 FilterObjectStr('issue__filters__stderr_regex', 

348 'Standard error regex used by the filter associated to an issue referencing this bug'), 

349 'filter_dmesg_regex': 

350 FilterObjectStr('issue__filters__dmesg_regex', 

351 'Regex for dmesg used by the filter associated to an issue referencing this bug'), 

352 'filter_added_on': 

353 FilterObjectDateTime('issue__issuefilterassociated__added_on', 

354 'Date at which the filter was associated to the issue referencing this bug'), 

355 'filter_covers_from': 

356 FilterObjectDateTime('issue__issuefilterassociated__covers_from', 

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

358 'filter_deleted_on': 

359 FilterObjectDateTime('issue__issuefilterassociated__deleted_on', 

360 'Date at which the filter was removed from the issue referencing this bug'), 

361 'filter_runconfigs_covered_count': 

362 FilterObjectInteger('issue__issuefilterassociated__runconfigs_covered_count', 

363 'Amount of run configurations covered by the filter associated to the issue referencing this bug'), # noqa 

364 'filter_runconfigs_affected_count': 

365 FilterObjectInteger('issue__issuefilterassociated__runconfigs_affected_count', 

366 'Amount of run configurations affected by the filter associated to the issue referencing this bug'), # noqa 

367 'filter_last_seen': 

368 FilterObjectDateTime('issue__issuefilterassociated__last_seen', 

369 'Date at which the filter associated to the issue referencing this bug last matched'), 

370 'filter_last_seen_runconfig_name': 

371 FilterObjectStr('issue__issuefilterassociated__last_seen_runconfig__name', 

372 'Run configuration which last matched the filter associated to the issue referencing this bug'), # noqa 

373 

374 'issue_description': FilterObjectStr('issue__description', 

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

376 'issue_filer_email': FilterObjectStr('issue__filer', 'Email address of the person who filed the issue'), 

377 'issue_added_on': FilterObjectDateTime('issue__added_on', 'Date at which the issue was created'), 

378 'issue_archived_on': FilterObjectDateTime('issue__archived_on', 'Date at which the issue was archived'), 

379 'issue_expected': FilterObjectBool('issue__expected', 'Is the issue expected?'), 

380 'issue_runconfigs_covered_count': FilterObjectInteger('issue__runconfigs_covered_count', 

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

382 'issue_runconfigs_affected_count': FilterObjectInteger('issue__runconfigs_affected_count', 

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

384 'issue_last_seen': FilterObjectDateTime('issue__last_seen', 'Date at which the issue was last seen'), 

385 'issue_last_seen_runconfig_name': FilterObjectStr('issue__last_seen_runconfig__name', 

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

387 

388 'tracker_name': FilterObjectStr('tracker__name', 'Name of the tracker hosting the bug'), 

389 'tracker_short_name': FilterObjectStr('tracker__short_name', 'Short name of the tracker which hosts the bug'), 

390 'tracker_type': FilterObjectStr('tracker__tracker_type', 'Type of the tracker which hosts the bug'), 

391 'bug_id': FilterObjectStr('bug_id', 'ID of the bug'), 

392 'title': FilterObjectStr('title', 'Title of the bug'), 

393 'created_on': FilterObjectDateTime('created', 'Date at which the bug was created'), 

394 'updated_on': FilterObjectDateTime('updated', 'Date at which the bug was last updated'), 

395 'closed_on': FilterObjectDateTime('closed', 'Date at which the bug was closed'), 

396 'creator_name': FilterObjectStr('creator__person__full_name', 'Name of the creator of the bug'), 

397 'creator_email': FilterObjectStr('creator__person__email', 'Email address of the creator of the bug'), 

398 'assignee_name': FilterObjectStr('assignee__person__full_name', 'Name of the assignee of the bug'), 

399 'assignee_email': FilterObjectStr('assignee__person__email', 'Email address of the assignee of the bug'), 

400 'product': FilterObjectStr('product', 'Product of the bug'), 

401 'component': FilterObjectStr('component', 'Component of the bug'), 

402 'priority': FilterObjectStr('priority', 'Priority of the bug'), 

403 'features': FilterObjectStr('features', 'Features affected (coma-separated list)'), 

404 'platforms': FilterObjectStr('platforms', 'Platforms affected (coma-separated list)'), 

405 'status': FilterObjectStr('status', 'Status of the bug (RESOLVED/FIXED, ...)'), 

406 'tags': FilterObjectStr('tags', 'Tags/labels associated to the bug (coma-separated list)'), 

407 'custom_fields': FilterObjectJSON('custom_fields', 'Custom Bug fields stored by key. Access using dot' 

408 'notation, e.g. custom_fields.build_id'), 

409 'parent_id': FilterObjectInteger('parent', 'ID of the parent bug from which this bug has been replicated'), 

410 } 

411 

412 UPDATE_PENDING_TIMEOUT = timedelta(minutes=45) 

413 

414 """ 

415 Stores a single bug entry, related to :model:`CIResults.BugTracker`. 

416 """ 

417 tracker = models.ForeignKey(BugTracker, on_delete=models.CASCADE, 

418 help_text="On which tracker is the bug stored") 

419 bug_id = models.CharField(max_length=20, 

420 help_text="The ID of the bug (e.g. 1234)") 

421 

422 # To be updated when polling 

423 parent = models.ForeignKey("self", null=True, blank=True, related_name="children", on_delete=models.CASCADE, 

424 help_text="This is the parent bug, if this bug is replicated. " 

425 "To be filled automatically") 

426 title = models.CharField(max_length=500, null=True, blank=True, 

427 help_text="Title of the bug report. To be filled automatically.") 

428 description = models.TextField(null=True, blank=True, help_text="Description of the bug report. To " 

429 "be filled automatically") 

430 created = models.DateTimeField(null=True, blank=True, 

431 help_text="Date of the creation of the bug report. To be filled automatically.") 

432 updated = models.DateTimeField(null=True, blank=True, 

433 help_text="Last update made to the bug report. To be filled automatically.") 

434 polled = models.DateTimeField(null=True, blank=True, 

435 help_text="Last time the bug was polled. To be filled automatically.") 

436 closed = models.DateTimeField(null=True, blank=True, 

437 help_text="Time at which the bug got closed. To be filled automatically.") 

438 creator = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE, null=True, blank=True, 

439 related_name="bug_creator_set", help_text="Person who wrote the initial bug report") 

440 assignee = models.ForeignKey(BugTrackerAccount, on_delete=models.CASCADE, null=True, blank=True, 

441 related_name="bug_assignee_set", help_text="Current assignee of the bug") 

442 product = models.CharField(max_length=50, null=True, blank=True, 

443 help_text="Product used for the bug filing (e.g. DRI) or NULL if not " 

444 "applicable. " 

445 "For GitLab, this is taken from labels matching 'product: $value'. " 

446 "To be filled automatically.") 

447 component = models.CharField(max_length=500, null=True, blank=True, 

448 help_text="Component used for the bug filing (e.g. DRM/Intel) or NULL if not " 

449 "applicable. " 

450 "For GitLab, this is taken from labels matching 'component: $value'. " 

451 "To be filled automatically.") 

452 priority = models.CharField(max_length=50, null=True, blank=True, 

453 help_text="Priority of the bug. For GitLab, this is taken from labels matching " 

454 "'priority::$value'. To be filled automatically.") 

455 severity = models.CharField(max_length=50, null=True, blank=True, 

456 help_text="Severity of the bug. For GitLab, this is taken from labels matching " 

457 "'severity::$value'. To be filled automatically.") 

458 features = models.CharField(max_length=500, null=True, blank=True, 

459 help_text="Coma-separated list of affected features or NULL if not applicable. " 

460 "For GitLab, this is taken from labels matching 'feature: $value'. " 

461 "To be filled automatically.") 

462 platforms = models.CharField(max_length=500, null=True, blank=True, 

463 help_text="Coma-separated list of affected platforms or NULL if not applicable. " 

464 "For GitLab, this is taken from labels matching 'platform: $value'. " 

465 "To be filled automatically.") 

466 status = models.CharField(max_length=100, null=True, blank=True, 

467 help_text="Status of the bug (e.g. RESOLVED/FIXED). To be filled automatically.") 

468 tags = models.TextField(null=True, blank=True, 

469 help_text="Stores a comma-separated list of Bug tags/labels. " 

470 "To be filled automatically") 

471 

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

473 

474 comments_polled = models.DateTimeField(null=True, blank=True, 

475 help_text="Last time the comments of the bug were polled. " 

476 "To be filled automatically.") 

477 

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

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

480 "willingness to update the bug") 

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

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

483 default=dict) 

484 

485 class Meta: 

486 constraints = [ 

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

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

489 ] 

490 

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

492 

493 @property 

494 def short_name(self): 

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

496 

497 @property 

498 def url(self): 

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

500 

501 @property 

502 def features_list(self): 

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

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

505 else: 

506 return [] 

507 

508 @property 

509 def platforms_list(self): 

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

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

512 else: 

513 return [] 

514 

515 @property 

516 def tags_list(self): 

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

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

519 else: 

520 return [] 

521 

522 @property 

523 def is_open(self): 

524 return self.tracker.is_bug_open(self) 

525 

526 @property 

527 def has_new_comments(self): 

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

529 

530 @cached_property 

531 def comments_cached(self): 

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

533 

534 @cached_property 

535 def involves(self): 

536 actors = defaultdict(lambda: 0) 

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

538 for comment in self.comments_cached: 

539 actors[comment.account] += 1 

540 

541 sorted_actors = OrderedDict() 

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

543 sorted_actors[account] = actors[account] 

544 

545 return sorted_actors 

546 

547 def __last_updated_by__(self, is_dev): 

548 last = None 

549 for comment in self.comments_cached: 

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

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

552 last = comment.created_on 

553 return last 

554 

555 @cached_property 

556 def last_updated_by_user(self): 

557 return self.__last_updated_by__(False) 

558 

559 @cached_property 

560 def last_updated_by_developer(self): 

561 return self.__last_updated_by__(True) 

562 

563 @cached_property 

564 def SLA(self): 

565 if self.priority is not None: 

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

567 else: 

568 return timedelta.max 

569 

570 @cached_property 

571 def SLA_deadline(self): 

572 if self.last_updated_by_developer is not None: 

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

574 if self.SLA != timedelta.max: 

575 return self.last_updated_by_developer + self.SLA 

576 else: 

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

578 else: 

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

580 return self.created + self.tracker.first_response_SLA 

581 

582 @cached_property 

583 def SLA_remaining_time(self): 

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

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

586 

587 @cached_property 

588 def SLA_remaining_str(self): 

589 rt = self.SLA_remaining_time 

590 if rt < timedelta(0): 

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

592 else: 

593 return "in " + str(rt) 

594 

595 @cached_property 

596 def effective_priority(self): 

597 return -self.SLA_remaining_time / self.SLA 

598 

599 @property 

600 def is_being_updated(self): 

601 if self.flagged_as_update_pending_on is None: 

602 return False 

603 else: 

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

605 

606 @property 

607 def update_pending_expires_in(self): 

608 if self.flagged_as_update_pending_on is None: 

609 return None 

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

611 

612 def clean(self): 

613 if self.custom_fields is None: 

614 return 

615 

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

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

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

619 

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

621 self.clean() 

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

623 

624 def update_from_dict(self, upd_dict): 

625 if not upd_dict: 

626 return 

627 

628 for field in upd_dict: 

629 # Disallow updating some critical fields 

630 if field in Bug.rd_only_fields: 

631 continue 

632 

633 if hasattr(self, field): 

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

635 

636 def poll(self, force_polling_comments=False): 

637 self.tracker.poll(self, force_polling_comments) 

638 

639 def add_comment(self, comment): 

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

641 

642 def create(self): 

643 try: 

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

645 except ValueError: # pragma: no cover 

646 traceback.print_exc() # pragma: no cover 

647 else: 

648 self.bug_id = id 

649 

650 def __str__(self): 

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

652 

653 

654class BugComment(models.Model, UserFiltrableMixin): 

655 filter_objects_to_db = { 

656 'filter_description': 

657 FilterObjectStr('bug__issue__filters__description', 

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

659 'filter_runconfig_tag_name': 

660 FilterObjectStr('bug__issue__filters__tags__name', 

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

662 'filter_machine_tag_name': 

663 FilterObjectStr('bug__issue__filters__machine_tags__name', 

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

665 'filter_machine_name': 

666 FilterObjectStr('bug__issue__filters__machines__name', 

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

668 'filter_test_name': 

669 FilterObjectStr('bug__issue__filters__tests__name', 

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

671 'filter_status_name': 

672 FilterObjectStr('bug__issue__filters__statuses__name', 

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

674 'filter_stdout_regex': 

675 FilterObjectStr('bug__issue__filters__stdout_regex', 

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

677 'filter_stderr_regex': 

678 FilterObjectStr('bug__issue__filters__stderr_regex', 

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

680 'filter_dmesg_regex': 

681 FilterObjectStr('bug__issue__filters__dmesg_regex', 

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

683 'filter_added_on': 

684 FilterObjectDateTime('bug__issue__issuefilterassociated__added_on', 

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

686 'filter_covers_from': 

687 FilterObjectDateTime('bug__issue__issuefilterassociated__covers_from', 

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

689 'filter_deleted_on': 

690 FilterObjectDateTime('bug__issue__issuefilterassociated__deleted_on', 

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

692 'filter_runconfigs_covered_count': 

693 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_covered_count', 

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

695 'filter_runconfigs_affected_count': 

696 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_affected_count', 

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

698 'filter_last_seen': 

699 FilterObjectDateTime('bug__issue__issuefilterassociated__last_seen', 

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

701 'filter_last_seen_runconfig_name': 

702 FilterObjectStr('bug__issue__issuefilterassociated__last_seen_runconfig__name', 

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

704 

705 'issue_description': FilterObjectStr('bug__issue__description', 

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

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

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

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

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

711 'issue_runconfigs_covered_count': FilterObjectInteger('bug__issue__runconfigs_covered_count', 

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

713 'issue_runconfigs_affected_count': FilterObjectInteger('bug__issue__runconfigs_affected_count', 

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

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

716 'issue_last_seen_runconfig_name': FilterObjectStr('bug__issue__last_seen_runconfig__name', 

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

718 

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

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

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

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

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

724 'bug_created_on': FilterObjectDateTime('bug__created', 

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

726 'bug_updated_on': FilterObjectDateTime('bug__updated', 

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

728 'bug_closed_on': FilterObjectDateTime('bug__closed', 

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

730 'bug_creator_name': FilterObjectStr('bug__creator__person__full_name', 

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

732 'bug_creator_email': FilterObjectStr('bug__creator__person__email', 

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

734 'bug_assignee_name': FilterObjectStr('bug__assignee__person__full_name', 

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

736 'bug_assignee_email': FilterObjectStr('bug__assignee__person__email', 

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

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

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

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

741 'bug_features': FilterObjectStr('bug__features', 

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

743 'bug_platforms': FilterObjectStr('bug__platforms', 

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

745 'bug_status': FilterObjectStr('bug__status', 

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

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

748 

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

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

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

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

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

754 } 

755 

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

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

758 

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

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

761 created_on = models.DateTimeField() 

762 

763 class Meta: 

764 constraints = [ 

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

766 ] 

767 

768 def __str__(self): 

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

770 

771 

772def script_validator(script): 

773 try: 

774 client = Client.get_or_create_instance(script) 

775 except (ValueError, IOError) as e: 

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

777 else: 

778 client.shutdown() 

779 return script 

780 

781 

782class ReplicationScript(models.Model): 

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

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

785 here - :ref:`replication-doc` 

786 """ 

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

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

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

790 created_on = models.DateTimeField(auto_now_add=True, 

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

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

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

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

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

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

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

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

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

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

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

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

803 

804 class Meta: 

805 constraints = [ 

806 UniqueConstraint( 

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

808 name='unique_source_tracker_destination_tracker', 

809 ), 

810 ] 

811 

812 def __str__(self): 

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

814 

815 

816# Software 

817class Component(models.Model): 

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

819 description = models.TextField() 

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

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

822 

823 def __str__(self): 

824 return self.name 

825 

826 

827class Build(models.Model): 

828 # Minimum information needed 

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

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

831 version = models.CharField(max_length=40) 

832 added_on = models.DateTimeField(auto_now=True) 

833 

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

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

836 

837 # Actual build information 

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

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

840 repo = models.CharField( 

841 max_length=200, 

842 null=True, 

843 blank=True, 

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

845 ) 

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

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

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

849 

850 @property 

851 def url(self): 

852 if self.upstream_url is not None: 

853 return self.upstream_url 

854 elif self.repo is not None: 

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

856 else: 

857 return self.version 

858 

859 def __str__(self): 

860 return self.name 

861 

862# Results 

863 

864 

865class VettableObjectMixin: 

866 @property 

867 def vetted(self): 

868 return self.vetted_on is not None 

869 

870 @transaction.atomic 

871 def vet(self): 

872 if self.vetted_on is not None: 

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

874 self.vetted_on = timezone.now() 

875 self.save() 

876 

877 @transaction.atomic 

878 def suppress(self): 

879 if self.vetted_on is None: 

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

881 self.vetted_on = None 

882 self.save() 

883 

884 

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

886 filter_objects_to_db = { 

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

888 'vetted_on': 

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

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

891 'first_runconfig': 

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

893 } 

894 name = models.CharField(max_length=150) 

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

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

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

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

899 added_on = models.DateTimeField(auto_now_add=True) 

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

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

902 

903 class Meta: 

904 ordering = ['name'] 

905 constraints = [ 

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

907 ] 

908 permissions = [ 

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

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

911 ] 

912 

913 def __str__(self): 

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

915 

916 @property 

917 def in_active_ifas(self): 

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

919 

920 @transaction.atomic 

921 def rename(self, new_name): 

922 # Get the matching test, or create it 

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

924 if new_test is None: 

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

926 public=self.public) 

927 else: 

928 new_test.public = self.public 

929 

930 new_test.vetted_on = self.vetted_on 

931 new_test.save() 

932 

933 # Now, update every active IFA 

934 for ifa in self.in_active_ifas: 

935 ifa.filter.tests.add(new_test) 

936 

937 

938class MachineTag(models.Model): 

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

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

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

942 

943 added_on = models.DateTimeField(auto_now_add=True) 

944 

945 class Meta: 

946 ordering = ['name'] 

947 

948 @cached_property 

949 def machines(self): 

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

951 

952 def __str__(self): 

953 return self.name 

954 

955 

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

957 filter_objects_to_db = { 

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

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

960 'vetted_on': 

961 FilterObjectDateTime('vetted_on', 

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

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

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

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

966 } 

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

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

969 

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

971 

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

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

974 

975 added_on = models.DateTimeField(auto_now_add=True) 

976 

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

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

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

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

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

982 

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

984 

985 color_hex = ColoredObjectMixin.color_hex 

986 

987 class Meta: 

988 ordering = ['name'] 

989 permissions = [ 

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

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

992 ] 

993 

994 @cached_property 

995 def tags_cached(self): 

996 return self.tags.all() 

997 

998 def __str__(self): 

999 return self.name 

1000 

1001 

1002class RunConfigTag(models.Model): 

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

1004 help_text="Unique name for the tag") 

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

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

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

1008 

1009 def __str__(self): 

1010 return self.name 

1011 

1012 

1013class RunConfig(models.Model): 

1014 filter_objects_to_db = { 

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

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

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

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

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

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

1021 

1022 # Through reverse accessors 

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

1024 'machine_tag': FilterObjectStr('testsuiterun__machine__tags__name', 

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

1026 } 

1027 

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

1029 tags = models.ManyToManyField(RunConfigTag) 

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

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

1032 

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

1034 

1035 builds = models.ManyToManyField(Build) 

1036 

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

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

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

1040 

1041 @cached_property 

1042 def tags_cached(self): 

1043 return self.tags.all() 

1044 

1045 @cached_property 

1046 def tags_ids_cached(self): 

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

1048 

1049 @cached_property 

1050 def builds_cached(self): 

1051 return self.builds.all() 

1052 

1053 @cached_property 

1054 def builds_ids_cached(self): 

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

1056 

1057 @cached_property 

1058 def public(self): 

1059 for tag in self.tags_cached: 

1060 if not tag.public: 

1061 return False 

1062 return True 

1063 

1064 @cached_property 

1065 def runcfg_history(self): 

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

1067 # the history of this particular run config 

1068 

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

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

1071 tags = self.tags_cached 

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

1073 

1074 @cached_property 

1075 def runcfg_history_offset(self): 

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

1077 if self.id == runcfg.id: 

1078 return i 

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

1080 

1081 def __str__(self): 

1082 return self.name 

1083 

1084 def update_statistics(self): 

1085 stats = [] 

1086 

1087 # Do not compute statistics for temporary runconfigs 

1088 if self.temporary: 

1089 return stats 

1090 

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

1092 

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

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

1095 for filter in filters: 

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

1097 matched_count=0) 

1098 

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

1100 if fs.covered_count < 1: 

1101 continue 

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

1103 fs.matched_count = len(matched_failures) 

1104 stats.append(fs) 

1105 

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

1107 with transaction.atomic(): 

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

1109 RunFilterStatistic.objects.bulk_create(stats) 

1110 

1111 return stats 

1112 

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

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

1115 no_compress=no_compress, query=query) 

1116 

1117 

1118class TestSuite(VettableObjectMixin, Component): 

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

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

1121 

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

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

1124 

1125 # Status to ignore for diffing 

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

1127 related_name='+') 

1128 

1129 class Meta: 

1130 permissions = [ 

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

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

1133 ] 

1134 

1135 @cached_property 

1136 def __acceptable_statuses__(self): 

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

1138 

1139 def __str__(self): 

1140 return self.name 

1141 

1142 def is_failure(self, status): 

1143 return status.id not in self.__acceptable_statuses__ 

1144 

1145 

1146class TestsuiteRun(models.Model, UserFiltrableMixin): 

1147 # For the FilterMixin. 

1148 filter_objects_to_db = { 

1149 'testsuite_name': FilterObjectStr('testsuite__name', 

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

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

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

1153 'runconfig_tag': FilterObjectStr('runconfig__tags__name', 

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

1155 'runconfig_added_on': FilterObjectDateTime('runconfig__added_on', 

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

1157 'runconfig_temporary': FilterObjectBool('runconfig__temporary', 

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

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

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

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

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

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

1164 'reported_on': FilterObjectDateTime('reported_on', 

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

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

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

1168 } 

1169 

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

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

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

1173 run_id = models.IntegerField() 

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

1175 

1176 start = models.DateTimeField() 

1177 duration = models.DurationField() 

1178 reported_on = models.DateTimeField(auto_now_add=True) 

1179 

1180 environment = models.TextField(blank=True, 

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

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

1183 log = models.TextField(blank=True) 

1184 

1185 class Meta: 

1186 constraints = [ 

1187 UniqueConstraint( 

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

1189 name='unique_testsuite_runconfig_machine_run_id', 

1190 ), 

1191 ] 

1192 ordering = ['start'] 

1193 

1194 def __str__(self): 

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

1196 

1197 

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

1199 filter_objects_to_db = { 

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

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

1202 } 

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

1204 name = models.CharField(max_length=20) 

1205 

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

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

1208 added_on = models.DateTimeField(auto_now_add=True) 

1209 

1210 color_hex = ColoredObjectMixin.color_hex 

1211 

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

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

1214 "The best possible is 0.") 

1215 

1216 class Meta: 

1217 constraints = [ 

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

1219 ] 

1220 verbose_name_plural = "Text Statuses" 

1221 permissions = [ 

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

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

1224 ] 

1225 

1226 @property 

1227 def is_failure(self): 

1228 return self.testsuite.is_failure(self) 

1229 

1230 @property 

1231 def is_notrun(self): 

1232 return self == self.testsuite.notrun_status 

1233 

1234 @property 

1235 def actual_severity(self): 

1236 if self.severity is not None: 

1237 return self.severity 

1238 elif self.is_notrun: 

1239 return 0 

1240 elif not self.is_failure: 

1241 return 1 

1242 else: 

1243 return 2 

1244 

1245 def __str__(self): 

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

1247 

1248 

1249class TestResultAssociatedManager(models.Manager): 

1250 def get_queryset(self): 

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

1252 'status', 'ts_run__machine', 

1253 'ts_run__machine__tags', 

1254 'ts_run__runconfig__tags', 

1255 'test') 

1256 

1257 

1258class TestResult(models.Model, UserFiltrableMixin): 

1259 # For the FilterMixin. 

1260 filter_objects_to_db = { 

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

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

1263 'runconfig_tag': FilterObjectStr('ts_run__runconfig__tags__name', 

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

1265 'runconfig_added_on': FilterObjectDateTime('ts_run__runconfig__added_on', 

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

1267 'runconfig_temporary': FilterObjectBool('ts_run__runconfig__temporary', 

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

1269 'build_name': FilterObjectStr('ts_run__runconfig__builds__name', 

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

1271 'build_added_on': FilterObjectDateTime('ts_run__runconfig__builds__added_on', 

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

1273 'component_name': FilterObjectStr('ts_run__runconfig__builds__component__name', 

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

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

1276 'machine_tag': FilterObjectStr('ts_run__machine__tags__name', 

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

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

1279 'testsuite_name': FilterObjectStr('status__testsuite__name', 

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

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

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

1283 'manually_filed_on': FilterObjectDateTime('known_failure__manually_associated_on', 

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

1285 'ifa_id': FilterObjectInteger('known_failure__matched_ifa_id', 

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

1287 'issue_id': FilterObjectInteger('known_failure__matched_ifa__issue_id', 

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

1289 'issue_expected': FilterObjectBool('known_failure__matched_ifa__issue__expected', 

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

1291 'filter_description': FilterObjectStr('known_failure__matched_ifa__issue__filters__description', 

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

1293 'filter_runconfig_tag_name': 

1294 FilterObjectStr('known_failure__matched_ifa__issue__filters__tags__name', 

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

1296 'filter_machine_tag_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machine_tags__name', 

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

1298 'filter_machine_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machines__name', 

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

1300 'filter_test_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__tests__name', 

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

1302 'filter_status_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__statuses__name', 

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

1304 'filter_stdout_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stdout_regex', 

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

1306 'filter_stderr_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stderr_regex', 

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

1308 'filter_dmesg_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__dmesg_regex', 

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

1310 'filter_added_on': FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__added_on', 

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

1312 'filter_covers_from': 

1313 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__covers_from', 

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

1315 'filter_deleted_on': 

1316 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__deleted_on', 

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

1318 'filter_runconfigs_covered_count': 

1319 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

1321 'filter_runconfigs_affected_count': 

1322 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

1324 'filter_last_seen': 

1325 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__last_seen', 

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

1327 'filter_last_seen_runconfig_name': 

1328 FilterObjectStr('known_failure__matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

1330 'bug_tracker_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__name', 

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

1332 'bug_tracker_short_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__short_name', 

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

1334 'bug_tracker_type': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__tracker_type', 

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

1336 'bug_id': FilterObjectStr('known_failure__matched_ifa__issue__bugs__bug_id', 

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

1338 'bug_title': FilterObjectStr('known_failure__matched_ifa__issue__bugs__title', 

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

1340 'bug_created_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__created', 

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

1342 'bug_closed_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__closed', 

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

1344 'bug_creator_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__full_name', 

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

1346 'bug_creator_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__email', 

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

1348 'bug_assignee_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__full_name', 

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

1350 'bug_assignee_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__email', 

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

1352 'bug_product': FilterObjectStr('known_failure__matched_ifa__issue__bugs__product', 

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

1354 'bug_component': FilterObjectStr('known_failure__matched_ifa__issue__bugs__component', 

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

1356 'bug_priority': FilterObjectStr('known_failure__matched_ifa__issue__bugs__priority', 

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

1358 'bug_features': FilterObjectStr('known_failure__matched_ifa__issue__bugs__features', 

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

1360 'bug_platforms': FilterObjectStr('known_failure__matched_ifa__issue__bugs__platforms', 

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

1362 'bug_status': FilterObjectStr('known_failure__matched_ifa__issue__bugs__status', 

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

1364 'bug_tags': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tags', 

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

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

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

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

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

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

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

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

1373 } 

1374 

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

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

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

1378 

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

1380 

1381 start = models.DateTimeField() 

1382 duration = models.DurationField() 

1383 

1384 command = models.CharField(max_length=500) 

1385 stdout = models.TextField(null=True) 

1386 stderr = models.TextField(null=True) 

1387 dmesg = models.TextField(null=True) 

1388 

1389 objects = models.Manager() 

1390 objects_ready_for_matching = TestResultAssociatedManager() 

1391 

1392 @cached_property 

1393 def is_failure(self): 

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

1395 

1396 @cached_property 

1397 def known_failures_cached(self): 

1398 return self.known_failures.all() 

1399 

1400 def __str__(self): 

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

1402 self.test.name, self.status) 

1403 

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

1405 

1406# Issues 

1407 

1408 

1409class IssueFilter(models.Model): 

1410 description = models.CharField(max_length=255, 

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

1412 

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

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

1415 "(leave empty to ignore tags)") 

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

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

1418 "(leave empty to ignore machines)") 

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

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

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

1422 "leave empty to ignore machines)") 

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

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

1425 "(leave empty to ignore tests)") 

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

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

1428 "ignore results)") 

1429 

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

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

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

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

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

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

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

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

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

1439 

1440 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1443 

1444 def delete(self): 

1445 self.hidden = True 

1446 

1447 @cached_property 

1448 def tags_cached(self): 

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

1450 

1451 @cached_property 

1452 def tags_ids_cached(self): 

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

1454 

1455 @cached_property 

1456 def __machines_cached__(self): 

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

1458 

1459 @cached_property 

1460 def __machine_tags_cached__(self): 

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

1462 

1463 @cached_property 

1464 def machines_cached(self): 

1465 machines = self.__machines_cached__.copy() 

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

1467 machines.add(machine) 

1468 return machines 

1469 

1470 @cached_property 

1471 def machines_ids_cached(self): 

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

1473 

1474 @cached_property 

1475 def tests_cached(self): 

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

1477 

1478 @cached_property 

1479 def tests_ids_cached(self): 

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

1481 

1482 @cached_property 

1483 def statuses_cached(self): 

1484 return set(self.statuses.all().select_related('testsuite')) 

1485 

1486 @cached_property 

1487 def statuses_ids_cached(self): 

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

1489 

1490 @cached_property 

1491 def stdout_regex_cached(self): 

1492 return re.compile(self.stdout_regex) 

1493 

1494 @cached_property 

1495 def stderr_regex_cached(self): 

1496 return re.compile(self.stderr_regex) 

1497 

1498 @cached_property 

1499 def dmesg_regex_cached(self): 

1500 return re.compile(self.dmesg_regex) 

1501 

1502 @cached_property 

1503 def covered_results(self): 

1504 return QueryParser( 

1505 TestResult, self.equivalent_user_query, ignore_fields=["stdout", "stderr", "dmesg", "status"] 

1506 ).objects 

1507 

1508 def covers(self, result): 

1509 if self.user_query: 

1510 return result in self.covered_results 

1511 

1512 # WARNING: Make sure to update the _to_user_query() method when changing 

1513 # the definition of "covers" 

1514 if len(self.tags_ids_cached) > 0: 

1515 if result.ts_run.runconfig.tags_ids_cached.isdisjoint(self.tags_ids_cached): 

1516 return False 

1517 

1518 if len(self.machines_ids_cached) > 0 or len(self.__machine_tags_cached__) > 0: 

1519 if result.ts_run.machine_id not in self.machines_ids_cached: 

1520 return False 

1521 

1522 if len(self.tests_ids_cached) > 0: 

1523 if result.test_id not in self.tests_ids_cached: 

1524 return False 

1525 

1526 return True 

1527 

1528 @cached_property 

1529 def matched_results(self): 

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

1531 

1532 @property 

1533 def matched_unknown_failures(self): 

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

1535 

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

1537 # WARNING: Make sure to update the _to_user_query() method when changing 

1538 # the definition of "matches" 

1539 if not skip_cover_test and not self.covers(result): 

1540 return False 

1541 

1542 if self.user_query: 

1543 return result in self.matched_results 

1544 

1545 if len(self.statuses_ids_cached) > 0: 

1546 if result.status_id not in self.statuses_ids_cached: 

1547 return False 

1548 

1549 if len(self.stdout_regex) > 0: 

1550 if self.stdout_regex_cached.search(result.stdout if result.stdout else "") is None: 

1551 return False 

1552 

1553 if len(self.stderr_regex) > 0: 

1554 if self.stderr_regex_cached.search(result.stderr if result.stderr else "") is None: 

1555 return False 

1556 

1557 if len(self.dmesg_regex) > 0: 

1558 if self.dmesg_regex_cached.search(result.dmesg if result.dmesg else "") is None: 

1559 return False 

1560 

1561 return True 

1562 

1563 @transaction.atomic 

1564 def replace(self, new_filter, user): 

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

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

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

1568 

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

1570 self.delete() 

1571 

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

1573 query = [] 

1574 

1575 if covers: 

1576 if len(self.tags_cached) > 0: 

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

1578 

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

1580 if len(self.__machines_cached__) > 0: 

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

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

1583 if len(self.__machine_tags_cached__) > 0: 

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

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

1586 

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

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

1589 elif len(self.__machines_cached__) > 0: 

1590 query.append(machines_query) 

1591 else: 

1592 query.append(machine_tags_query) 

1593 

1594 if len(self.tests_cached) > 0: 

1595 tests_query = [] 

1596 

1597 # group the tests by testsuite 

1598 testsuites = defaultdict(set) 

1599 for test in self.tests_cached: 

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

1601 

1602 # create the sub-queries 

1603 for testsuite in testsuites: 

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

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

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

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

1608 

1609 if matches: 

1610 if len(self.statuses_cached) > 0: 

1611 status_query = [] 

1612 

1613 # group the statuses by testsuite 

1614 testsuites = defaultdict(set) 

1615 for status in self.statuses_cached: 

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

1617 

1618 # create the sub-queries 

1619 for testsuite in testsuites: 

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

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

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

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

1624 

1625 if len(self.stdout_regex) > 0: 

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

1627 

1628 if len(self.stderr_regex) > 0: 

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

1630 

1631 if len(self.dmesg_regex) > 0: 

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

1633 

1634 return " AND ".join(query) 

1635 

1636 @cached_property 

1637 def equivalent_user_query(self) -> str: 

1638 if self.user_query: 

1639 return self.user_query 

1640 return self._to_user_query() 

1641 

1642 def __str__(self): 

1643 return self.description 

1644 

1645 

1646class Rate: 

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

1648 self._type_str = type_str 

1649 self._affected = affected 

1650 self._total = total 

1651 

1652 @property 

1653 def rate(self): 

1654 if self._total > 0: 

1655 return self._affected / self._total 

1656 else: 

1657 return 0 

1658 

1659 def __str__(self): 

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

1661 self._total, 

1662 self._type_str, 

1663 self.rate * 100.0) 

1664 

1665 

1666class IssueFilterAssociatedManager(models.Manager): 

1667 def get_queryset(self): 

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

1669 'filter__machine_tags', 

1670 'filter__machines', 

1671 'filter__tests', 

1672 'filter__statuses', 

1673 'filter') 

1674 

1675 

1676class IssueFilterAssociated(models.Model): 

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

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

1679 

1680 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1683 

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

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

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

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

1688 

1689 # Statistics cache 

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

1691 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1692 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1695 

1696 objects = models.Manager() 

1697 objects_ready_for_matching = IssueFilterAssociatedManager() 

1698 

1699 @property 

1700 def active(self): 

1701 return self.deleted_on is None 

1702 

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

1704 if self.deleted_on is not None: 

1705 return 

1706 

1707 if now is not None: 

1708 self.deleted_on = now 

1709 else: 

1710 self.deleted_on = timezone.now() 

1711 

1712 self.deleted_by = user 

1713 self.save() 

1714 

1715 @cached_property 

1716 def __runfilter_stats_covered__(self): 

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

1718 

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

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

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

1722 # earliest of these two events. 

1723 start_time = self.added_on 

1724 if self.covers_from < start_time: 

1725 start_time = self.covers_from 

1726 

1727 if self.deleted_on is not None: 

1728 return objs.filter(runconfig__added_on__gte=start_time, 

1729 runconfig__added_on__lt=self.deleted_on, 

1730 covered_count__gt=0, 

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

1732 else: 

1733 return objs.filter(runconfig__added_on__gte=start_time, 

1734 covered_count__gt=0, 

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

1736 

1737 @cached_property 

1738 def runconfigs_covered(self): 

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

1740 

1741 @cached_property 

1742 def runconfigs_affected(self): 

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

1744 

1745 @property 

1746 def covered_results(self): 

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

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

1749 

1750 def _add_missing_stats(self): 

1751 # Find the list of runconfig we have stats for 

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

1753 

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

1755 stats = dict() 

1756 results = ( 

1757 self.covered_results.exclude(ts_run__runconfig__id__in=runconfigs_done) 

1758 .filter(ts_run__runconfig__temporary=False) 

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

1760 ) 

1761 for result in results: 

1762 runconfig = result.ts_run.runconfig 

1763 fs = stats.get(runconfig) 

1764 if fs is None: 

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

1766 matched_count=0, covered_count=0) 

1767 

1768 fs.covered_count += 1 

1769 

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

1771 # query to also check if they matched. 

1772 # 

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

1774 # list of ids we got previously 

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

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

1777 for result in query: 

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

1779 

1780 # Save the statistics objects 

1781 for fs in stats.values(): 

1782 fs.save() 

1783 

1784 def update_statistics(self): 

1785 # drop all the caches 

1786 try: 

1787 del self.__runfilter_stats_covered__ 

1788 del self.runconfigs_covered 

1789 del self.runconfigs_affected 

1790 except AttributeError: 

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

1792 pass 

1793 

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

1795 req = req.order_by("result__ts_run__runconfig__added_on") 

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

1797 if oldest_failure is not None: 

1798 self.covers_from = oldest_failure 

1799 

1800 # get the list of runconfigs needing update 

1801 self._add_missing_stats() 

1802 

1803 self.runconfigs_covered_count = len(self.runconfigs_covered) 

1804 self.runconfigs_affected_count = len(self.runconfigs_affected) 

1805 

1806 # Find when the issue was last seen 

1807 for stats in self.__runfilter_stats_covered__: 

1808 if stats.matched_count > 0: 

1809 self.last_seen = stats.runconfig.added_on 

1810 self.last_seen_runconfig = stats.runconfig 

1811 break 

1812 

1813 # Update the statistics atomically in the DB 

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

1815 cur_ifa.update(covers_from=self.covers_from, 

1816 runconfigs_covered_count=self.runconfigs_covered_count, 

1817 runconfigs_affected_count=self.runconfigs_affected_count, 

1818 last_seen=self.last_seen, 

1819 last_seen_runconfig=self.last_seen_runconfig) 

1820 

1821 @property 

1822 def failure_rate(self): 

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

1824 

1825 @property 

1826 def activity_period(self): 

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

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

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

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

1831 

1832 if not self.active: 

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

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

1835 naturaltime(self.deleted_on), deleted_by, 

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

1837 else: 

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

1839 

1840 def __str__(self): 

1841 if self.deleted_on is not None: 

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

1843 else: 

1844 delete_on = "" 

1845 

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

1847 

1848 

1849class Issue(models.Model, UserFiltrableMixin): 

1850 filter_objects_to_db = { 

1851 'filter_description': FilterObjectStr('filters__description', 

1852 'Description of what the filter matches'), 

1853 'filter_runconfig_tag_name': FilterObjectStr('filters__tags__name', 

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

1855 'filter_machine_tag_name': FilterObjectStr('filters__machine_tags__name', 

1856 'Machine tag matched by the filter'), 

1857 'filter_machine_name': FilterObjectStr('filters__machines__name', 

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

1859 'filter_test_name': FilterObjectStr('filters__tests__name', 

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

1861 'filter_status_name': FilterObjectStr('filters__statuses__name', 

1862 'Status matched by the filter'), 

1863 'filter_stdout_regex': FilterObjectStr('filters__stdout_regex', 

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

1865 'filter_stderr_regex': FilterObjectStr('filters__stderr_regex', 

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

1867 'filter_dmesg_regex': FilterObjectStr('filters__dmesg_regex', 

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

1869 'filter_added_on': FilterObjectDateTime('issuefilterassociated__added_on', 

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

1871 'filter_covers_from': FilterObjectDateTime('issuefilterassociated__covers_from', 

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

1873 'filter_deleted_on': FilterObjectDateTime('issuefilterassociated__deleted_on', 

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

1875 'filter_runconfigs_covered_count': FilterObjectInteger('issuefilterassociated__runconfigs_covered_count', 

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

1877 'filter_runconfigs_affected_count': FilterObjectInteger('issuefilterassociated__runconfigs_affected_count', 

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

1879 'filter_last_seen': FilterObjectDateTime('issuefilterassociated__last_seen', 

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

1881 'filter_last_seen_runconfig_name': FilterObjectStr('issuefilterassociated__last_seen_runconfig__name', 

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

1883 

1884 'bug_tracker_name': FilterObjectStr('bugs__tracker__name', 

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

1886 'bug_tracker_short_name': FilterObjectStr('bugs__tracker__short_name', 

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

1888 'bug_tracker_type': FilterObjectStr('bugs__tracker__tracker_type', 

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

1890 'bug_id': FilterObjectStr('bugs__bug_id', 

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

1892 'bug_title': FilterObjectStr('bugs__title', 

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

1894 'bug_created_on': FilterObjectDateTime('bugs__created', 

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

1896 'bug_updated_on': FilterObjectDateTime('bugs__updated', 

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

1898 'bug_closed_on': FilterObjectDateTime('bugs__closed', 

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

1900 'bug_creator_name': FilterObjectStr('bugs__creator__person__full_name', 

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

1902 'bug_creator_email': FilterObjectStr('bugs__creator__person__email', 

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

1904 'bug_assignee_name': FilterObjectStr('bugs__assignee__person__full_name', 

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

1906 'bug_assignee_email': FilterObjectStr('bugs__assignee__person__email', 

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

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

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

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

1911 'bug_features': FilterObjectStr('bugs__features', 

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

1913 'bug_platforms': FilterObjectStr('bugs__platforms', 

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

1915 'bug_status': FilterObjectStr('bugs__status', 

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

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

1918 'bug_tags': FilterObjectStr('bugs__tags', 

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

1920 

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

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

1923 

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

1925 

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

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

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

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

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

1931 'runconfigs_covered_count': FilterObjectInteger('runconfigs_covered_count', 

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

1933 'runconfigs_affected_count': FilterObjectInteger('runconfigs_affected_count', 

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

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

1936 'last_seen_runconfig_name': FilterObjectStr('last_seen_runconfig__name', 

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

1938 } 

1939 

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

1941 bugs = models.ManyToManyField(Bug) 

1942 

1943 description = models.TextField(blank=True) 

1944 filer = models.EmailField() # DEPRECATED 

1945 

1946 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1949 

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

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

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

1953 

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

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

1956 

1957 # Statistics cache 

1958 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1959 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1962 

1963 class Meta: 

1964 permissions = [ 

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

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

1967 

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

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

1970 ] 

1971 

1972 @property 

1973 def archived(self): 

1974 return self.archived_on is not None 

1975 

1976 def hide(self): 

1977 self.expected = True 

1978 self.save() 

1979 

1980 def show(self): 

1981 self.expected = False 

1982 self.save() 

1983 

1984 @cached_property 

1985 def active_filters(self): 

1986 if self.archived: 

1987 deleted_on = self.archived_on 

1988 else: 

1989 deleted_on = None 

1990 

1991 if hasattr(self, 'ifas_cached'): 

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

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

1994 else: 

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

1996 deleted_on=deleted_on) 

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

1998 

1999 @cached_property 

2000 def all_filters(self): 

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

2002 

2003 @property 

2004 def past_filters(self): 

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

2006 

2007 @cached_property 

2008 def bugs_cached(self): 

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

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

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

2012 

2013 @cached_property 

2014 def covers_from(self): 

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

2016 

2017 @cached_property 

2018 def __runfilter_stats_covered__(self): 

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

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

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

2022 covered_count__gt=0, 

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

2024 if self.archived: 

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

2026 

2027 return objs 

2028 

2029 @cached_property 

2030 def runconfigs_covered(self): 

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

2032 

2033 @cached_property 

2034 def runconfigs_affected(self): 

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

2036 # to the set of affected ones 

2037 runconfigs_affected = set() 

2038 for runfilter in self.__runfilter_stats_covered__: 

2039 if runfilter.matched_count > 0: 

2040 runconfigs_affected.add(runfilter.runconfig) 

2041 

2042 return runconfigs_affected 

2043 

2044 def update_statistics(self): 

2045 self.runconfigs_covered_count = len(self.runconfigs_covered) 

2046 self.runconfigs_affected_count = len(self.runconfigs_affected) 

2047 

2048 # Find when the issue was last seen 

2049 for stats in self.__runfilter_stats_covered__: 

2050 if stats.matched_count > 0: 

2051 self.last_seen = stats.runconfig.added_on 

2052 self.last_seen_runconfig = stats.runconfig 

2053 break 

2054 

2055 # Update the statistics atomically in the DB 

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

2057 cur_issue.update(runconfigs_covered_count=self.runconfigs_covered_count, 

2058 runconfigs_affected_count=self.runconfigs_affected_count, 

2059 last_seen=self.last_seen, 

2060 last_seen_runconfig=self.last_seen_runconfig) 

2061 

2062 @property 

2063 def failure_rate(self): 

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

2065 

2066 def matches(self, result): 

2067 if self.archived: 

2068 return False 

2069 

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

2071 if e.filter.matches(result): 

2072 return True 

2073 return False 

2074 

2075 def archive(self, user): 

2076 if self.archived: 

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

2078 

2079 with transaction.atomic(): 

2080 now = timezone.now() 

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

2082 e.delete(user, now) 

2083 self.archived_on = now 

2084 self.archived_by = user 

2085 self.save() 

2086 

2087 # Post a comment 

2088 comment = render_to_string("CIResults/issue_archived.txt", {"issue": self}) 

2089 self.comment_on_all_bugs(comment) 

2090 

2091 def restore(self): 

2092 if not self.archived: 

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

2094 

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

2096 with transaction.atomic(): 

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

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

2099 

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

2101 self.archived_on = None 

2102 self.archived_by = None 

2103 self.save() 

2104 

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

2106 self.update_statistics() 

2107 

2108 # Post a comment 

2109 comment = render_to_string("CIResults/issue_restored.txt", {"issue": self}) 

2110 self.comment_on_all_bugs(comment) 

2111 

2112 @transaction.atomic 

2113 def set_bugs(self, bugs): 

2114 if self.archived: 

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

2116 

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

2118 self.bugs.clear() 

2119 

2120 for bug in bugs: 

2121 # Make sure the bug exists in the database first 

2122 if bug.id is None: 

2123 bug.save() 

2124 

2125 # Add it to the relation 

2126 self.bugs.add(bug) 

2127 

2128 # Get rid of the cached bugs 

2129 try: 

2130 del self.bugs_cached 

2131 except AttributeError: 

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

2133 pass 

2134 

2135 def _assign_to_known_failures(self, unknown_failures, ifa): 

2136 now = timezone.now() 

2137 new_matched_failures = [] 

2138 for failure in unknown_failures: 

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

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

2141 manually_associated_on=now, 

2142 filing_delay=filing_delay) 

2143 new_matched_failures.append(kf) 

2144 failure.delete() 

2145 

2146 ifa.update_statistics() 

2147 

2148 return new_matched_failures 

2149 

2150 def __filter_add__(self, filter, user): 

2151 # Make sure the filter exists in the database first 

2152 if filter.id is None: 

2153 filter.save() 

2154 

2155 # Create the association between the filter and the issue 

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

2157 

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

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

2160 matched_unknown_failures = ( 

2161 filter.matched_unknown_failures.select_related("result") 

2162 .prefetch_related("result__ts_run") 

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

2164 ) 

2165 return self._assign_to_known_failures(matched_unknown_failures, ifa) 

2166 

2167 def comment_on_all_bugs(self, comment): 

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

2169 

2170 try: 

2171 for bug in self.bugs_cached: 

2172 bug.add_comment(comment) 

2173 except Exception: # pragma: no cover 

2174 traceback.print_exc() # pragma: no cover 

2175 

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

2177 if self.archived: 

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

2179 

2180 with transaction.atomic(): 

2181 # First, add the new filter 

2182 failures = self.__filter_add__(new_filter, user) 

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

2184 

2185 # Delete all active associations of the old filter 

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

2187 for e in assocs: 

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

2189 

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

2191 self.update_statistics() 

2192 

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

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

2195 old_filter.equivalent_user_query != new_filter.equivalent_user_query or 

2196 len(new_matched_failures) > 0): 

2197 comment = render_to_string("CIResults/issue_replace_filter_comment.txt", 

2198 {"issue": self, "old_filter": old_filter, 

2199 "new_filter": new_filter, "new_matched_failures": new_matched_failures, 

2200 "user": user}) 

2201 self.comment_on_all_bugs(comment) 

2202 

2203 def set_filters(self, filters, user): 

2204 if self.archived: 

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

2206 

2207 with transaction.atomic(): 

2208 removed_ifas = set() 

2209 new_filters = dict() 

2210 

2211 # Query the set of issues that we currently have 

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

2213 

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

2215 now = timezone.now() 

2216 for e in assocs: 

2217 if e.filter not in filters: 

2218 e.delete(user, now) 

2219 removed_ifas.add(e) 

2220 

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

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

2223 for filter in filters: 

2224 if filter.id not in cur_filters_ids: 

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

2226 

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

2228 self.update_statistics() 

2229 

2230 # Get rid of the cached filters 

2231 try: 

2232 del self.active_filters 

2233 except AttributeError: 

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

2235 pass 

2236 

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

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

2239 comment = render_to_string("CIResults/issue_set_filters_comment.txt", 

2240 {"issue": self, "removed_ifas": removed_ifas, 

2241 "new_filters": new_filters, "user": user}) 

2242 self.comment_on_all_bugs(comment + "") # Add an empty string to get a string instead of safetext 

2243 

2244 @transaction.atomic 

2245 def merge_issues(self, issues, user): 

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

2247 

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

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

2250 

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

2252 # archiving them 

2253 for issue in issues: 

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

2255 new_issue_filters.append(filter) 

2256 issue.archive(user) 

2257 

2258 # Set the new list of filters 

2259 self.set_filters(new_issue_filters, user) 

2260 

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

2262 self.update_statistics() 

2263 

2264 def __str__(self): 

2265 bugs = self.bugs.all() 

2266 if len(bugs) == 0: 

2267 return "Issue: <empty>" 

2268 elif len(bugs) == 1: 

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

2270 else: 

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

2272 

2273 

2274class KnownFailure(models.Model, UserFiltrableMixin): 

2275 # For the FilterMixin. 

2276 filter_objects_to_db = { 

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

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

2279 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', 

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

2281 'runconfig_added_on': FilterObjectDateTime('result__ts_run__runconfig__added_on', 

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

2283 'runconfig_temporary': FilterObjectBool('result__ts_run__runconfig__temporary', 

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

2285 'build_name': FilterObjectStr('result__ts_run__runconfig__builds__name', 

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

2287 'build_added_on': FilterObjectDateTime('result__ts_run__runconfig__builds__added_on', 

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

2289 'component_name': FilterObjectStr('result__ts_run__runconfig__builds__component__name', 

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

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

2292 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', 

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

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

2295 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

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

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

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

2299 'manually_filed_on': FilterObjectDateTime('manually_associated_on', 

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

2301 'ifa_id': FilterObjectInteger('matched_ifa_id', 

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

2303 'issue_id': FilterObjectInteger('matched_ifa__issue_id', 

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

2305 'issue_expected': FilterObjectBool('matched_ifa__issue__expected', 

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

2307 'filter_description': FilterObjectStr('matched_ifa__issue__filters__description', 

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

2309 'filter_runconfig_tag_name': 

2310 FilterObjectStr('matched_ifa__issue__filters__tags__name', 

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

2312 'filter_machine_tag_name': FilterObjectStr('matched_ifa__issue__filters__machine_tags__name', 

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

2314 'filter_machine_name': FilterObjectStr('matched_ifa__issue__filters__machines__name', 

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

2316 'filter_test_name': FilterObjectStr('matched_ifa__issue__filters__tests__name', 

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

2318 'filter_status_name': FilterObjectStr('matched_ifa__issue__filters__statuses__name', 

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

2320 'filter_stdout_regex': FilterObjectStr('matched_ifa__issue__filters__stdout_regex', 

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

2322 'filter_stderr_regex': FilterObjectStr('matched_ifa__issue__filters__stderr_regex', 

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

2324 'filter_dmesg_regex': FilterObjectStr('matched_ifa__issue__filters__dmesg_regex', 

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

2326 'filter_added_on': FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__added_on', 

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

2328 'filter_covers_from': 

2329 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__covers_from', 

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

2331 'filter_deleted_on': 

2332 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__deleted_on', 

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

2334 'filter_runconfigs_covered_count': 

2335 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

2337 'filter_runconfigs_affected_count': 

2338 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

2340 'filter_last_seen': 

2341 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__last_seen', 

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

2343 'filter_last_seen_runconfig_name': 

2344 FilterObjectStr('matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

2346 'bug_tracker_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__name', 

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

2348 'bug_tracker_short_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__short_name', 

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

2350 'bug_tracker_type': FilterObjectStr('matched_ifa__issue__bugs__tracker__tracker_type', 

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

2352 'bug_id': FilterObjectStr('matched_ifa__issue__bugs__bug_id', 

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

2354 'bug_title': FilterObjectStr('matched_ifa__issue__bugs__title', 

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

2356 'bug_created_on': FilterObjectDateTime('matched_ifa__issue__bugs__created', 

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

2358 'bug_closed_on': FilterObjectDateTime('matched_ifa__issue__bugs__closed', 

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

2360 'bug_creator_name': FilterObjectStr('matched_ifa__issue__bugs__creator__person__full_name', 

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

2362 'bug_creator_email': FilterObjectStr('matched_ifa__issue__bugs__creator__person__email', 

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

2364 'bug_assignee_name': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__full_name', 

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

2366 'bug_assignee_email': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__email', 

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

2368 'bug_product': FilterObjectStr('matched_ifa__issue__bugs__product', 

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

2370 'bug_component': FilterObjectStr('matched_ifa__issue__bugs__component', 

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

2372 'bug_priority': FilterObjectStr('matched_ifa__issue__bugs__priority', 

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

2374 'bug_features': FilterObjectStr('matched_ifa__issue__bugs__features', 

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

2376 'bug_platforms': FilterObjectStr('matched_ifa__issue__bugs__platforms', 

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

2378 'bug_status': FilterObjectStr('matched_ifa__issue__bugs__status', 

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

2380 'bug_tags': FilterObjectStr('matched_ifa__issue__bugs__tags', 

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

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

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

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

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

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

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

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

2389 } 

2390 

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

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

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

2394 

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

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

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

2398 

2399 @classmethod 

2400 def _runconfig_index(cls, covered_list, runconfig): 

2401 try: 

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

2403 return covered.index(runconfig) 

2404 except ValueError: 

2405 return None 

2406 

2407 @cached_property 

2408 def covered_runconfigs_since_for_issue(self): 

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

2410 self.result.ts_run.runconfig) 

2411 

2412 @cached_property 

2413 def covered_runconfigs_since_for_filter(self): 

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

2415 self.result.ts_run.runconfig) 

2416 

2417 def __str__(self): 

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

2419 

2420 

2421class UnknownFailure(models.Model, UserFiltrableMixin): 

2422 filter_objects_to_db = { 

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

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

2425 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

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

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

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

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

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

2431 'bug_title': FilterObjectStr('matched_archived_ifas__issue__bugs__description', 

2432 "Description of bug associated to failure"), 

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

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

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

2436 } 

2437 # We cannot have two UnknownFailure for the same result 

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

2439 related_name="unknown_failure") 

2440 

2441 matched_archived_ifas = models.ManyToManyField(IssueFilterAssociated) 

2442 

2443 @cached_property 

2444 def matched_archived_ifas_cached(self): 

2445 return self.matched_archived_ifas.all() 

2446 

2447 @cached_property 

2448 def matched_issues(self): 

2449 issues = set() 

2450 for e in self.matched_archived_ifas_cached: 

2451 issues.add(e.issue) 

2452 return issues 

2453 

2454 def __str__(self): 

2455 return str(self.result) 

2456 

2457 

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

2459class RunFilterStatistic(models.Model): 

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

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

2462 

2463 covered_count = models.PositiveIntegerField() 

2464 matched_count = models.PositiveIntegerField() 

2465 

2466 class Meta: 

2467 constraints = [ 

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

2469 ] 

2470 

2471 def __str__(self): 

2472 if self.covered_count > 0: 

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

2474 else: 

2475 perc = 0 

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

2477 self.runconfig, 

2478 self.matched_count, 

2479 self.covered_count, 

2480 perc)