Coverage for CIResults/models.py: 95%

1112 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-19 09:20 +0000

1from django.core.validators import RegexValidator, URLValidator 

2from django.core.exceptions import ValidationError 

3from django.conf import settings 

4from django.contrib.auth import get_user_model 

5from django.contrib.auth.models import User 

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

7from django.utils.functional import cached_property 

8from django.db import models, transaction 

9from django.utils.timesince import timesince 

10from django.utils import timezone 

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

12from django.template.loader import render_to_string 

13 

14from .runconfigdiff import RunConfigDiff 

15from .filtering import ( 

16 FilterObjectBool, 

17 FilterObjectDateTime, 

18 FilterObjectDuration, 

19 FilterObjectInteger, 

20 FilterObjectJSON, 

21 FilterObjectModel, 

22 FilterObjectStr, 

23 QueryParser, 

24 QueryParserPython, 

25 UserFiltrableMixin, 

26) 

27from .sandbox.io import Client 

28 

29from collections import defaultdict, OrderedDict 

30 

31from datetime import timedelta 

32import hashlib 

33import traceback 

34import re 

35 

36 

37def get_sentinel_user(): 

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

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

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

41 

42 

43class ColoredObjectMixin: 

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

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

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

47 validators=[RegexValidator( 

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

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

50 )]) 

51 

52 @cached_property 

53 def color(self): 

54 if self.color_hex is not None: 

55 return self.color_hex 

56 

57 # Generate a random color 

58 blake2 = hashlib.blake2b() 

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

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

61 

62 

63# Bug tracking 

64 

65 

66class BugTrackerSLA(models.Model): 

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

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

69 "SLA for (case insensitive)") 

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

71 

72 class Meta: 

73 constraints = [ 

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

75 ] 

76 verbose_name_plural = "Bug Tracker SLAs" 

77 

78 def __str__(self): 

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

80 

81 

82class Person(models.Model): 

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

84 email = models.EmailField(null=True) 

85 

86 added_on = models.DateTimeField(auto_now_add=True) 

87 

88 def __str__(self): 

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

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

91 

92 if has_full_name and has_email: 

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

94 elif has_full_name: 

95 return self.full_name 

96 elif has_email: 

97 return self.email 

98 else: 

99 return "(No name or email)" 

100 

101 

102class BugTrackerAccount(models.Model): 

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

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

105 

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

107 

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

109 

110 class Meta: 

111 constraints = [ 

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

113 ] 

114 

115 def __str__(self): 

116 return str(self.person) 

117 

118 

119class BugTracker(models.Model): 

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

121 

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

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

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

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

126 "gitlab (project id): 1234 " 

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

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

129 separator = models.CharField(max_length=1, 

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

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

132 

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

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

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

136 

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

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

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

140 

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

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

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

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

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

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

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

148 # Stats 

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

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

151 

152 # Configurations 

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

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

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

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

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

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

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

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

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

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

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

164 "from this BugTracker, e.g. " 

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

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

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

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

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

170 

171 @cached_property 

172 def SLAs_cached(self): 

173 slas = dict() 

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

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

176 return slas 

177 

178 @cached_property 

179 def tracker(self): 

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

181 

182 if self.tracker_type == "bugzilla": 

183 return Bugzilla(self) 

184 elif self.tracker_type == "gitlab": 

185 return GitLab(self) 

186 elif self.tracker_type == "jira": 

187 return Jira(self) 

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

189 return Untracked(self) 

190 else: 

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

192 

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

194 self.tracker.poll(bug, force_polling_comments) 

195 bug.polled = self.tracker_time 

196 

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

198 if bugs is None: 

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

200 

201 for bug in bugs: 

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

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

204 return 

205 

206 try: 

207 self.poll(bug) 

208 except Exception: # pragma: no cover 

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

210 traceback.print_exc() # pragma: no cover 

211 

212 bug.save() 

213 

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

215 # all bugs have been updated. 

216 self.polled = self.tracker_time 

217 self.save() 

218 

219 @property 

220 def tracker_time(self): 

221 return self.tracker._get_tracker_time() 

222 

223 def to_tracker_tz(self, dt): 

224 return self.tracker._to_tracker_tz(dt) 

225 

226 @property 

227 def open_statuses(self): 

228 return self.tracker.open_statuses 

229 

230 def is_bug_open(self, bug): 

231 return bug.status in self.open_statuses 

232 

233 @property 

234 def components_followed_list(self): 

235 if self.components_followed is None: 

236 return [] 

237 else: 

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

239 

240 @transaction.atomic 

241 def get_or_create_bugs(self, ids): 

242 new_bugs = set() 

243 

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

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

246 for bug_id in ids - known_bug_ids: 

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

248 

249 return set(known_bugs).union(new_bugs) 

250 

251 def __set_tracker_to_bugs__(self, bugs): 

252 for bug in bugs: 

253 bug.tracker = self 

254 return bugs 

255 

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

257 def open_bugs(self): 

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

259 

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

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

262 status=self.open_statuses) 

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

264 

265 return self.__set_tracker_to_bugs__(open_bugs) 

266 

267 def bugs_in_issues(self): 

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

269 bugs_in_issues = set() 

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

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

272 bugs_in_issues.add(bug) 

273 

274 return bugs_in_issues 

275 

276 def followed_bugs(self): 

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

278 

279 def updated_bugs(self): 

280 if not self.polled: 

281 return self.followed_bugs() 

282 

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

284 

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

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

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

288 updated_since=polled_time) 

289 not_upd_ids = all_bug_ids - all_upd_bug_ids 

290 

291 open_bug_ids = set() 

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

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

294 status=self.open_statuses) 

295 

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

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

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

299 

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

301 upd_bugs = self.get_or_create_bugs(bug_ids) 

302 

303 return self.__set_tracker_to_bugs__(upd_bugs) 

304 

305 def unreplicated_bugs(self): 

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

307 parent__isnull=True, 

308 tracker=self, 

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

310 

311 unrep_bugs = self.get_or_create_bugs(unrep_bug_ids) 

312 

313 return self.__set_tracker_to_bugs__(unrep_bugs) 

314 

315 def clean(self): 

316 if self.custom_fields_map is None: 

317 return 

318 

319 for field in self.custom_fields_map: 

320 if self.custom_fields_map[field] is None: 

321 continue 

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

323 

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

325 self.clean() 

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

327 

328 def __str__(self): 

329 return self.name 

330 

331 

332class Bug(models.Model, UserFiltrableMixin): 

333 filter_objects_to_db = { 

334 'filter_description': 

335 FilterObjectStr('issue__filters__description', 

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

337 'filter_runconfig_tag_name': 

338 FilterObjectStr('issue__filters__tags__name', 

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

340 'filter_machine_tag_name': 

341 FilterObjectStr('issue__filters__machine_tags__name', 

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

343 'filter_machine_name': 

344 FilterObjectStr('issue__filters__machines__name', 

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

346 'filter_test_name': 

347 FilterObjectStr('issue__filters__tests__name', 

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

349 'filter_status_name': 

350 FilterObjectStr('issue__filters__statuses__name', 

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

352 'filter_stdout_regex': 

353 FilterObjectStr('issue__filters__stdout_regex', 

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

355 'filter_stderr_regex': 

356 FilterObjectStr('issue__filters__stderr_regex', 

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

358 'filter_dmesg_regex': 

359 FilterObjectStr('issue__filters__dmesg_regex', 

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

361 'filter_added_on': 

362 FilterObjectDateTime('issue__issuefilterassociated__added_on', 

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

364 'filter_covers_from': 

365 FilterObjectDateTime('issue__issuefilterassociated__covers_from', 

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

367 'filter_deleted_on': 

368 FilterObjectDateTime('issue__issuefilterassociated__deleted_on', 

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

370 'filter_runconfigs_covered_count': 

371 FilterObjectInteger('issue__issuefilterassociated__runconfigs_covered_count', 

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

373 'filter_runconfigs_affected_count': 

374 FilterObjectInteger('issue__issuefilterassociated__runconfigs_affected_count', 

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

376 'filter_last_seen': 

377 FilterObjectDateTime('issue__issuefilterassociated__last_seen', 

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

379 'filter_last_seen_runconfig_name': 

380 FilterObjectStr('issue__issuefilterassociated__last_seen_runconfig__name', 

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

382 

383 'issue_description': FilterObjectStr('issue__description', 

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

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

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

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

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

389 'issue_runconfigs_covered_count': FilterObjectInteger('issue__runconfigs_covered_count', 

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

391 'issue_runconfigs_affected_count': FilterObjectInteger('issue__runconfigs_affected_count', 

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

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

394 'issue_last_seen_runconfig_name': FilterObjectStr('issue__last_seen_runconfig__name', 

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

396 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

419 } 

420 

421 UPDATE_PENDING_TIMEOUT = timedelta(minutes=45) 

422 

423 """ 

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

425 """ 

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

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

428 bug_id = models.CharField(max_length=20, 

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

430 

431 # To be updated when polling 

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

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

434 "To be filled automatically") 

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

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

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

438 "be filled automatically") 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

453 "applicable. " 

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

455 "To be filled automatically.") 

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

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

458 "applicable. " 

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

460 "To be filled automatically.") 

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

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

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

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

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

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

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

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

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

470 "To be filled automatically.") 

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

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

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

474 "To be filled automatically.") 

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

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

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

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

479 "To be filled automatically") 

480 

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

482 

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

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

485 "To be filled automatically.") 

486 

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

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

489 "willingness to update the bug") 

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

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

492 default=dict) 

493 

494 class Meta: 

495 constraints = [ 

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

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

498 ] 

499 

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

501 

502 @property 

503 def short_name(self): 

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

505 

506 @property 

507 def url(self): 

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

509 

510 @property 

511 def features_list(self): 

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

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

514 else: 

515 return [] 

516 

517 @property 

518 def platforms_list(self): 

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

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

521 else: 

522 return [] 

523 

524 @property 

525 def tags_list(self): 

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

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

528 else: 

529 return [] 

530 

531 @property 

532 def is_open(self): 

533 return self.tracker.is_bug_open(self) 

534 

535 @property 

536 def has_new_comments(self): 

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

538 

539 @cached_property 

540 def comments_cached(self): 

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

542 

543 @cached_property 

544 def involves(self): 

545 actors = defaultdict(lambda: 0) 

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

547 for comment in self.comments_cached: 

548 actors[comment.account] += 1 

549 

550 sorted_actors = OrderedDict() 

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

552 sorted_actors[account] = actors[account] 

553 

554 return sorted_actors 

555 

556 def __last_updated_by__(self, is_dev): 

557 last = None 

558 for comment in self.comments_cached: 

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

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

561 last = comment.created_on 

562 return last 

563 

564 @cached_property 

565 def last_updated_by_user(self): 

566 return self.__last_updated_by__(False) 

567 

568 @cached_property 

569 def last_updated_by_developer(self): 

570 return self.__last_updated_by__(True) 

571 

572 @cached_property 

573 def SLA(self): 

574 if self.priority is not None: 

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

576 else: 

577 return timedelta.max 

578 

579 @cached_property 

580 def SLA_deadline(self): 

581 if self.last_updated_by_developer is not None: 

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

583 if self.SLA != timedelta.max: 

584 return self.last_updated_by_developer + self.SLA 

585 else: 

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

587 else: 

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

589 return self.created + self.tracker.first_response_SLA 

590 

591 @cached_property 

592 def SLA_remaining_time(self): 

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

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

595 

596 @cached_property 

597 def SLA_remaining_str(self): 

598 rt = self.SLA_remaining_time 

599 if rt < timedelta(0): 

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

601 else: 

602 return "in " + str(rt) 

603 

604 @cached_property 

605 def effective_priority(self): 

606 return -self.SLA_remaining_time / self.SLA 

607 

608 @property 

609 def is_being_updated(self): 

610 if self.flagged_as_update_pending_on is None: 

611 return False 

612 else: 

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

614 

615 @property 

616 def update_pending_expires_in(self): 

617 if self.flagged_as_update_pending_on is None: 

618 return None 

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

620 

621 def clean(self): 

622 if self.custom_fields is None: 

623 return 

624 

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

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

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

628 

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

630 self.clean() 

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

632 

633 def update_from_dict(self, upd_dict): 

634 if not upd_dict: 

635 return 

636 

637 for field in upd_dict: 

638 # Disallow updating some critical fields 

639 if field in Bug.rd_only_fields: 

640 continue 

641 

642 if hasattr(self, field): 

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

644 

645 def poll(self, force_polling_comments=False): 

646 self.tracker.poll(self, force_polling_comments) 

647 

648 def add_comment(self, comment): 

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

650 

651 def create(self): 

652 try: 

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

654 except ValueError: # pragma: no cover 

655 traceback.print_exc() # pragma: no cover 

656 else: 

657 self.bug_id = id 

658 

659 def __str__(self): 

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

661 

662 

663class BugComment(models.Model, UserFiltrableMixin): 

664 filter_objects_to_db = { 

665 'filter_description': 

666 FilterObjectStr('bug__issue__filters__description', 

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

668 'filter_runconfig_tag_name': 

669 FilterObjectStr('bug__issue__filters__tags__name', 

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

671 'filter_machine_tag_name': 

672 FilterObjectStr('bug__issue__filters__machine_tags__name', 

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

674 'filter_machine_name': 

675 FilterObjectStr('bug__issue__filters__machines__name', 

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

677 'filter_test_name': 

678 FilterObjectStr('bug__issue__filters__tests__name', 

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

680 'filter_status_name': 

681 FilterObjectStr('bug__issue__filters__statuses__name', 

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

683 'filter_stdout_regex': 

684 FilterObjectStr('bug__issue__filters__stdout_regex', 

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

686 'filter_stderr_regex': 

687 FilterObjectStr('bug__issue__filters__stderr_regex', 

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

689 'filter_dmesg_regex': 

690 FilterObjectStr('bug__issue__filters__dmesg_regex', 

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

692 'filter_added_on': 

693 FilterObjectDateTime('bug__issue__issuefilterassociated__added_on', 

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

695 'filter_covers_from': 

696 FilterObjectDateTime('bug__issue__issuefilterassociated__covers_from', 

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

698 'filter_deleted_on': 

699 FilterObjectDateTime('bug__issue__issuefilterassociated__deleted_on', 

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

701 'filter_runconfigs_covered_count': 

702 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_covered_count', 

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

704 'filter_runconfigs_affected_count': 

705 FilterObjectInteger('bug__issue__issuefilterassociated__runconfigs_affected_count', 

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

707 'filter_last_seen': 

708 FilterObjectDateTime('bug__issue__issuefilterassociated__last_seen', 

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

710 'filter_last_seen_runconfig_name': 

711 FilterObjectStr('bug__issue__issuefilterassociated__last_seen_runconfig__name', 

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

713 

714 'issue_description': FilterObjectStr('bug__issue__description', 

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

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

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

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

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

720 'issue_runconfigs_covered_count': FilterObjectInteger('bug__issue__runconfigs_covered_count', 

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

722 'issue_runconfigs_affected_count': FilterObjectInteger('bug__issue__runconfigs_affected_count', 

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

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

725 'issue_last_seen_runconfig_name': FilterObjectStr('bug__issue__last_seen_runconfig__name', 

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

727 

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

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

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

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

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

733 'bug_created_on': FilterObjectDateTime('bug__created', 

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

735 'bug_updated_on': FilterObjectDateTime('bug__updated', 

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

737 'bug_closed_on': FilterObjectDateTime('bug__closed', 

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

739 'bug_creator_name': FilterObjectStr('bug__creator__person__full_name', 

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

741 'bug_creator_email': FilterObjectStr('bug__creator__person__email', 

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

743 'bug_assignee_name': FilterObjectStr('bug__assignee__person__full_name', 

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

745 'bug_assignee_email': FilterObjectStr('bug__assignee__person__email', 

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

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

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

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

750 'bug_features': FilterObjectStr('bug__features', 

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

752 'bug_platforms': FilterObjectStr('bug__platforms', 

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

754 'bug_status': FilterObjectStr('bug__status', 

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

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

757 

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

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

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

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

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

763 } 

764 

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

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

767 

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

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

770 created_on = models.DateTimeField() 

771 

772 class Meta: 

773 constraints = [ 

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

775 ] 

776 

777 def __str__(self): 

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

779 

780 

781def script_validator(script): 

782 try: 

783 client = Client.get_or_create_instance(script) 

784 except (ValueError, IOError) as e: 

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

786 else: 

787 client.shutdown() 

788 return script 

789 

790 

791class ReplicationScript(models.Model): 

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

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

794 here - :ref:`replication-doc` 

795 """ 

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

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

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

799 created_on = models.DateTimeField(auto_now_add=True, 

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

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

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

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

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

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

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

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

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

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

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

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

812 

813 class Meta: 

814 constraints = [ 

815 UniqueConstraint( 

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

817 name='unique_source_tracker_destination_tracker', 

818 ), 

819 ] 

820 

821 def __str__(self): 

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

823 

824 

825# Software 

826class Component(models.Model): 

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

828 description = models.TextField() 

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

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

831 

832 def __str__(self): 

833 return self.name 

834 

835 

836class Build(models.Model): 

837 # Minimum information needed 

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

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

840 version = models.CharField(max_length=40) 

841 added_on = models.DateTimeField(auto_now=True) 

842 

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

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

845 

846 # Actual build information 

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

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

849 repo = models.CharField( 

850 max_length=200, 

851 null=True, 

852 blank=True, 

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

854 ) 

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

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

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

858 

859 @property 

860 def url(self): 

861 if self.upstream_url is not None: 

862 return self.upstream_url 

863 elif self.repo is not None: 

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

865 else: 

866 return self.version 

867 

868 def __str__(self): 

869 return self.name 

870 

871# Results 

872 

873 

874class VettableObjectMixin: 

875 @property 

876 def vetted(self): 

877 return self.vetted_on is not None 

878 

879 @transaction.atomic 

880 def vet(self): 

881 if self.vetted_on is not None: 

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

883 self.vetted_on = timezone.now() 

884 self.save() 

885 

886 @transaction.atomic 

887 def suppress(self): 

888 if self.vetted_on is None: 

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

890 self.vetted_on = None 

891 self.save() 

892 

893 

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

895 filter_objects_to_db = { 

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

897 'vetted_on': 

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

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

900 'first_runconfig': 

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

902 } 

903 name = models.CharField(max_length=150) 

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

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

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

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

908 added_on = models.DateTimeField(auto_now_add=True) 

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

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

911 

912 class Meta: 

913 ordering = ['name'] 

914 constraints = [ 

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

916 ] 

917 permissions = [ 

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

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

920 ] 

921 

922 def __str__(self): 

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

924 

925 @property 

926 def in_active_ifas(self): 

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

928 

929 @transaction.atomic 

930 def rename(self, new_name): 

931 # Get the matching test, or create it 

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

933 if new_test is None: 

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

935 public=self.public) 

936 else: 

937 new_test.public = self.public 

938 

939 new_test.vetted_on = self.vetted_on 

940 new_test.save() 

941 

942 # Now, update every active IFA 

943 for ifa in self.in_active_ifas: 

944 ifa.filter.tests.add(new_test) 

945 

946 

947class MachineTag(models.Model): 

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

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

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

951 

952 added_on = models.DateTimeField(auto_now_add=True) 

953 

954 class Meta: 

955 ordering = ['name'] 

956 

957 @cached_property 

958 def machines(self): 

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

960 

961 def __str__(self): 

962 return self.name 

963 

964 

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

966 filter_objects_to_db = { 

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

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

969 'vetted_on': 

970 FilterObjectDateTime('vetted_on', 

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

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

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

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

975 } 

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

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

978 

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

980 

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

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

983 

984 added_on = models.DateTimeField(auto_now_add=True) 

985 

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

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

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

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

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

991 

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

993 

994 color_hex = ColoredObjectMixin.color_hex 

995 

996 class Meta: 

997 ordering = ['name'] 

998 permissions = [ 

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

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

1001 ] 

1002 

1003 @cached_property 

1004 def tags_cached(self): 

1005 return self.tags.all() 

1006 

1007 def __str__(self): 

1008 return self.name 

1009 

1010 

1011class RunConfigTag(models.Model): 

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

1013 help_text="Unique name for the tag") 

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

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

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

1017 

1018 def __str__(self): 

1019 return self.name 

1020 

1021 

1022class RunConfig(models.Model): 

1023 filter_objects_to_db = { 

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

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

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

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

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

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

1030 

1031 # Through reverse accessors 

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

1033 'machine_tag': FilterObjectStr('testsuiterun__machine__tags__name', 

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

1035 } 

1036 

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

1038 tags = models.ManyToManyField(RunConfigTag) 

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

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

1041 

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

1043 

1044 builds = models.ManyToManyField(Build) 

1045 

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

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

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

1049 

1050 @cached_property 

1051 def tags_cached(self): 

1052 return self.tags.all() 

1053 

1054 @cached_property 

1055 def tags_ids_cached(self): 

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

1057 

1058 @cached_property 

1059 def builds_cached(self): 

1060 return self.builds.all() 

1061 

1062 @cached_property 

1063 def builds_ids_cached(self): 

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

1065 

1066 @cached_property 

1067 def public(self): 

1068 for tag in self.tags_cached: 

1069 if not tag.public: 

1070 return False 

1071 return True 

1072 

1073 @cached_property 

1074 def runcfg_history(self): 

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

1076 # the history of this particular run config 

1077 

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

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

1080 tags = self.tags_cached 

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

1082 

1083 @cached_property 

1084 def runcfg_history_offset(self): 

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

1086 if self.id == runcfg.id: 

1087 return i 

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

1089 

1090 def __str__(self): 

1091 return self.name 

1092 

1093 def update_statistics(self): 

1094 stats = [] 

1095 

1096 # Do not compute statistics for temporary runconfigs 

1097 if self.temporary: 

1098 return stats 

1099 

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

1101 

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

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

1104 for filter in filters: 

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

1106 matched_count=0) 

1107 

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

1109 if fs.covered_count < 1: 

1110 continue 

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

1112 fs.matched_count = len(matched_failures) 

1113 stats.append(fs) 

1114 

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

1116 with transaction.atomic(): 

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

1118 RunFilterStatistic.objects.bulk_create(stats) 

1119 

1120 return stats 

1121 

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

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

1124 no_compress=no_compress, query=query) 

1125 

1126 

1127class TestSuite(VettableObjectMixin, Component): 

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

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

1130 

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

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

1133 

1134 # Status to ignore for diffing 

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

1136 related_name='+') 

1137 

1138 class Meta: 

1139 permissions = [ 

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

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

1142 ] 

1143 

1144 @cached_property 

1145 def __acceptable_statuses__(self): 

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

1147 

1148 def __str__(self): 

1149 return self.name 

1150 

1151 def is_failure(self, status): 

1152 return status.id not in self.__acceptable_statuses__ 

1153 

1154 

1155class TestsuiteRun(models.Model, UserFiltrableMixin): 

1156 # For the FilterMixin. 

1157 filter_objects_to_db = { 

1158 'testsuite_name': FilterObjectStr('testsuite__name', 

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

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

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

1162 'runconfig_tag': FilterObjectStr('runconfig__tags__name', 

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

1164 'runconfig_added_on': FilterObjectDateTime('runconfig__added_on', 

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

1166 'runconfig_temporary': FilterObjectBool('runconfig__temporary', 

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

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

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

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

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

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

1173 'reported_on': FilterObjectDateTime('reported_on', 

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

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

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

1177 } 

1178 

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

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

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

1182 run_id = models.IntegerField() 

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

1184 

1185 start = models.DateTimeField() 

1186 duration = models.DurationField() 

1187 reported_on = models.DateTimeField(auto_now_add=True) 

1188 

1189 environment = models.TextField(blank=True, 

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

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

1192 log = models.TextField(blank=True) 

1193 

1194 class Meta: 

1195 constraints = [ 

1196 UniqueConstraint( 

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

1198 name='unique_testsuite_runconfig_machine_run_id', 

1199 ), 

1200 ] 

1201 ordering = ['start'] 

1202 

1203 def __str__(self): 

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

1205 

1206 

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

1208 filter_objects_to_db = { 

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

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

1211 } 

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

1213 name = models.CharField(max_length=20) 

1214 

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

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

1217 added_on = models.DateTimeField(auto_now_add=True) 

1218 

1219 color_hex = ColoredObjectMixin.color_hex 

1220 

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

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

1223 "The best possible is 0.") 

1224 

1225 class Meta: 

1226 constraints = [ 

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

1228 ] 

1229 verbose_name_plural = "Text Statuses" 

1230 permissions = [ 

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

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

1233 ] 

1234 

1235 @property 

1236 def is_failure(self): 

1237 return self.testsuite.is_failure(self) 

1238 

1239 @property 

1240 def is_notrun(self): 

1241 return self == self.testsuite.notrun_status 

1242 

1243 @property 

1244 def actual_severity(self): 

1245 if self.severity is not None: 

1246 return self.severity 

1247 elif self.is_notrun: 

1248 return 0 

1249 elif not self.is_failure: 

1250 return 1 

1251 else: 

1252 return 2 

1253 

1254 def __str__(self): 

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

1256 

1257 

1258class TestResultAssociatedManager(models.Manager): 

1259 def get_queryset(self): 

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

1261 'status', 'ts_run__machine', 

1262 'ts_run__machine__tags', 

1263 'ts_run__runconfig__tags', 

1264 'test') 

1265 

1266 

1267class TestResult(models.Model, UserFiltrableMixin): 

1268 # For the FilterMixin. 

1269 filter_objects_to_db = { 

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

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

1272 'runconfig_tag': FilterObjectStr('ts_run__runconfig__tags__name', 

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

1274 'runconfig_added_on': FilterObjectDateTime('ts_run__runconfig__added_on', 

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

1276 'runconfig_temporary': FilterObjectBool('ts_run__runconfig__temporary', 

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

1278 'build_name': FilterObjectStr('ts_run__runconfig__builds__name', 

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

1280 'build_added_on': FilterObjectDateTime('ts_run__runconfig__builds__added_on', 

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

1282 'component_name': FilterObjectStr('ts_run__runconfig__builds__component__name', 

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

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

1285 'machine_tag': FilterObjectStr('ts_run__machine__tags__name', 

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

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

1288 'testsuite_name': FilterObjectStr('status__testsuite__name', 

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

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

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

1292 'manually_filed_on': FilterObjectDateTime('known_failure__manually_associated_on', 

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

1294 'ifa_id': FilterObjectInteger('known_failure__matched_ifa_id', 

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

1296 'issue_id': FilterObjectInteger('known_failure__matched_ifa__issue_id', 

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

1298 'issue_expected': FilterObjectBool('known_failure__matched_ifa__issue__expected', 

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

1300 'filter_description': FilterObjectStr('known_failure__matched_ifa__issue__filters__description', 

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

1302 'filter_runconfig_tag_name': 

1303 FilterObjectStr('known_failure__matched_ifa__issue__filters__tags__name', 

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

1305 'filter_machine_tag_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machine_tags__name', 

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

1307 'filter_machine_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__machines__name', 

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

1309 'filter_test_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__tests__name', 

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

1311 'filter_status_name': FilterObjectStr('known_failure__matched_ifa__issue__filters__statuses__name', 

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

1313 'filter_stdout_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stdout_regex', 

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

1315 'filter_stderr_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__stderr_regex', 

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

1317 'filter_dmesg_regex': FilterObjectStr('known_failure__matched_ifa__issue__filters__dmesg_regex', 

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

1319 'filter_added_on': FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__added_on', 

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

1321 'filter_covers_from': 

1322 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__covers_from', 

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

1324 'filter_deleted_on': 

1325 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__deleted_on', 

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

1327 'filter_runconfigs_covered_count': 

1328 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

1330 'filter_runconfigs_affected_count': 

1331 FilterObjectInteger('known_failure__matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

1333 'filter_last_seen': 

1334 FilterObjectDateTime('known_failure__matched_ifa__issue__issuefilterassociated__last_seen', 

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

1336 'filter_last_seen_runconfig_name': 

1337 FilterObjectStr('known_failure__matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

1339 'bug_tracker_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__name', 

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

1341 'bug_tracker_short_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__short_name', 

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

1343 'bug_tracker_type': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tracker__tracker_type', 

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

1345 'bug_id': FilterObjectStr('known_failure__matched_ifa__issue__bugs__bug_id', 

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

1347 'bug_title': FilterObjectStr('known_failure__matched_ifa__issue__bugs__title', 

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

1349 'bug_created_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__created', 

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

1351 'bug_closed_on': FilterObjectDateTime('known_failure__matched_ifa__issue__bugs__closed', 

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

1353 'bug_creator_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__full_name', 

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

1355 'bug_creator_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__creator__person__email', 

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

1357 'bug_assignee_name': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__full_name', 

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

1359 'bug_assignee_email': FilterObjectStr('known_failure__matched_ifa__issue__bugs__assignee__person__email', 

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

1361 'bug_product': FilterObjectStr('known_failure__matched_ifa__issue__bugs__product', 

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

1363 'bug_component': FilterObjectStr('known_failure__matched_ifa__issue__bugs__component', 

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

1365 'bug_priority': FilterObjectStr('known_failure__matched_ifa__issue__bugs__priority', 

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

1367 'bug_features': FilterObjectStr('known_failure__matched_ifa__issue__bugs__features', 

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

1369 'bug_platforms': FilterObjectStr('known_failure__matched_ifa__issue__bugs__platforms', 

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

1371 'bug_status': FilterObjectStr('known_failure__matched_ifa__issue__bugs__status', 

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

1373 'bug_tags': FilterObjectStr('known_failure__matched_ifa__issue__bugs__tags', 

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

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

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

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

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

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

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

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

1382 } 

1383 

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

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

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

1387 

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

1389 

1390 start = models.DateTimeField() 

1391 duration = models.DurationField() 

1392 

1393 command = models.CharField(max_length=500) 

1394 stdout = models.TextField(null=True) 

1395 stderr = models.TextField(null=True) 

1396 dmesg = models.TextField(null=True) 

1397 

1398 objects = models.Manager() 

1399 objects_ready_for_matching = TestResultAssociatedManager() 

1400 

1401 @cached_property 

1402 def is_failure(self): 

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

1404 

1405 @cached_property 

1406 def known_failures_cached(self): 

1407 return self.known_failures.all() 

1408 

1409 def __str__(self): 

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

1411 self.test.name, self.status) 

1412 

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

1414 

1415# Issues 

1416 

1417 

1418class IssueFilter(models.Model): 

1419 description = models.CharField(max_length=255, 

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

1421 

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

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

1424 "(leave empty to ignore tags)") 

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

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

1427 "(leave empty to ignore machines)") 

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

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

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

1431 "leave empty to ignore machines)") 

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

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

1434 "(leave empty to ignore tests)") 

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

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

1437 "ignore results)") 

1438 

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

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

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

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

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

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

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

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

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

1448 

1449 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1452 

1453 def delete(self): 

1454 self.hidden = True 

1455 

1456 @cached_property 

1457 def tags_cached(self): 

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

1459 

1460 @cached_property 

1461 def tags_ids_cached(self): 

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

1463 

1464 @cached_property 

1465 def __machines_cached__(self): 

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

1467 

1468 @cached_property 

1469 def __machine_tags_cached__(self): 

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

1471 

1472 @cached_property 

1473 def machines_cached(self): 

1474 machines = self.__machines_cached__.copy() 

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

1476 machines.add(machine) 

1477 return machines 

1478 

1479 @cached_property 

1480 def machines_ids_cached(self): 

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

1482 

1483 @cached_property 

1484 def tests_cached(self): 

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

1486 

1487 @cached_property 

1488 def tests_ids_cached(self): 

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

1490 

1491 @cached_property 

1492 def statuses_cached(self): 

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

1494 

1495 @cached_property 

1496 def statuses_ids_cached(self): 

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

1498 

1499 @cached_property 

1500 def stdout_regex_cached(self): 

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

1502 

1503 @cached_property 

1504 def stderr_regex_cached(self): 

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

1506 

1507 @cached_property 

1508 def dmesg_regex_cached(self): 

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

1510 

1511 @cached_property 

1512 def covered_results(self): 

1513 return QueryParser( 

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

1515 ).objects 

1516 

1517 @cached_property 

1518 def __covers_function(self): 

1519 parser = QueryParserPython( 

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

1521 ) 

1522 if not parser.is_valid: 

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

1524 return parser.matching_fn 

1525 

1526 def covers(self, result): 

1527 try: 

1528 return self.__covers_function(result) 

1529 except ValueError as err: 

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

1531 return False 

1532 

1533 @cached_property 

1534 def matched_results(self): 

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

1536 

1537 @property 

1538 def matched_unknown_failures(self): 

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

1540 

1541 @cached_property 

1542 def __matches_function(self): 

1543 parser = QueryParserPython(TestResult, self.equivalent_user_query) 

1544 if not parser.is_valid: 

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

1546 return parser.matching_fn 

1547 

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

1549 try: 

1550 return self.__matches_function(result) 

1551 except ValueError as err: 

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

1553 return False 

1554 

1555 @transaction.atomic 

1556 def replace(self, new_filter, user): 

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

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

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

1560 

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

1562 self.delete() 

1563 

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

1565 query = [] 

1566 

1567 if covers: 

1568 if len(self.tags_cached) > 0: 

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

1570 

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

1572 if len(self.__machines_cached__) > 0: 

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

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

1575 if len(self.__machine_tags_cached__) > 0: 

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

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

1578 

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

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

1581 elif len(self.__machines_cached__) > 0: 

1582 query.append(machines_query) 

1583 else: 

1584 query.append(machine_tags_query) 

1585 

1586 if len(self.tests_cached) > 0: 

1587 tests_query = [] 

1588 

1589 # group the tests by testsuite 

1590 testsuites = defaultdict(set) 

1591 for test in self.tests_cached: 

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

1593 

1594 # create the sub-queries 

1595 for testsuite in testsuites: 

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

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

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

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

1600 

1601 if matches: 

1602 if len(self.statuses_cached) > 0: 

1603 status_query = [] 

1604 

1605 # group the statuses by testsuite 

1606 testsuites = defaultdict(set) 

1607 for status in self.statuses_cached: 

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

1609 

1610 # create the sub-queries 

1611 for testsuite in testsuites: 

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

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

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

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

1616 

1617 if len(self.stdout_regex) > 0: 

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

1619 

1620 if len(self.stderr_regex) > 0: 

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

1622 

1623 if len(self.dmesg_regex) > 0: 

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

1625 

1626 return " AND ".join(query) 

1627 

1628 @cached_property 

1629 def equivalent_user_query(self) -> str: 

1630 if self.user_query: 

1631 return self.user_query 

1632 return self._to_user_query() 

1633 

1634 def __str__(self): 

1635 return self.description 

1636 

1637 

1638class Rate: 

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

1640 self._type_str = type_str 

1641 self._affected = affected 

1642 self._total = total 

1643 

1644 @property 

1645 def rate(self): 

1646 if self._total > 0: 

1647 return self._affected / self._total 

1648 else: 

1649 return 0 

1650 

1651 def __str__(self): 

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

1653 self._total, 

1654 self._type_str, 

1655 self.rate * 100.0) 

1656 

1657 

1658class IssueFilterAssociatedManager(models.Manager): 

1659 def get_queryset(self): 

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

1661 'filter__machine_tags', 

1662 'filter__machines', 

1663 'filter__tests', 

1664 'filter__statuses', 

1665 'filter') 

1666 

1667 

1668class IssueFilterAssociated(models.Model): 

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

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

1671 

1672 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1675 

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

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

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

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

1680 

1681 # Statistics cache 

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

1683 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1684 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1687 

1688 objects = models.Manager() 

1689 objects_ready_for_matching = IssueFilterAssociatedManager() 

1690 

1691 @property 

1692 def active(self): 

1693 return self.deleted_on is None 

1694 

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

1696 if self.deleted_on is not None: 

1697 return 

1698 

1699 if now is not None: 

1700 self.deleted_on = now 

1701 else: 

1702 self.deleted_on = timezone.now() 

1703 

1704 self.deleted_by = user 

1705 self.save() 

1706 

1707 @cached_property 

1708 def __runfilter_stats_covered__(self): 

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

1710 

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

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

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

1714 # earliest of these two events. 

1715 start_time = self.added_on 

1716 if self.covers_from < start_time: 

1717 start_time = self.covers_from 

1718 

1719 if self.deleted_on is not None: 

1720 return objs.filter(runconfig__added_on__gte=start_time, 

1721 runconfig__added_on__lt=self.deleted_on, 

1722 covered_count__gt=0, 

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

1724 else: 

1725 return objs.filter(runconfig__added_on__gte=start_time, 

1726 covered_count__gt=0, 

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

1728 

1729 @cached_property 

1730 def runconfigs_covered(self): 

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

1732 

1733 @cached_property 

1734 def runconfigs_affected(self): 

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

1736 

1737 @property 

1738 def covered_results(self): 

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

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

1741 

1742 def _add_missing_stats(self): 

1743 # Find the list of runconfig we have stats for 

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

1745 

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

1747 stats = dict() 

1748 results = ( 

1749 self.covered_results.exclude(ts_run__runconfig__id__in=runconfigs_done) 

1750 .filter(ts_run__runconfig__temporary=False) 

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

1752 ) 

1753 for result in results: 

1754 runconfig = result.ts_run.runconfig 

1755 fs = stats.get(runconfig) 

1756 if fs is None: 

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

1758 matched_count=0, covered_count=0) 

1759 

1760 fs.covered_count += 1 

1761 

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

1763 # query to also check if they matched. 

1764 # 

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

1766 # list of ids we got previously 

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

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

1769 for result in query: 

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

1771 

1772 # Save the statistics objects 

1773 for fs in stats.values(): 

1774 fs.save() 

1775 

1776 def update_statistics(self): 

1777 # drop all the caches 

1778 try: 

1779 del self.__runfilter_stats_covered__ 

1780 del self.runconfigs_covered 

1781 del self.runconfigs_affected 

1782 except AttributeError: 

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

1784 pass 

1785 

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

1787 req = req.order_by("result__ts_run__runconfig__added_on") 

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

1789 if oldest_failure is not None: 

1790 self.covers_from = oldest_failure 

1791 

1792 # get the list of runconfigs needing update 

1793 self._add_missing_stats() 

1794 

1795 self.runconfigs_covered_count = len(self.runconfigs_covered) 

1796 self.runconfigs_affected_count = len(self.runconfigs_affected) 

1797 

1798 # Find when the issue was last seen 

1799 for stats in self.__runfilter_stats_covered__: 

1800 if stats.matched_count > 0: 

1801 self.last_seen = stats.runconfig.added_on 

1802 self.last_seen_runconfig = stats.runconfig 

1803 break 

1804 

1805 # Update the statistics atomically in the DB 

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

1807 cur_ifa.update(covers_from=self.covers_from, 

1808 runconfigs_covered_count=self.runconfigs_covered_count, 

1809 runconfigs_affected_count=self.runconfigs_affected_count, 

1810 last_seen=self.last_seen, 

1811 last_seen_runconfig=self.last_seen_runconfig) 

1812 

1813 @property 

1814 def failure_rate(self): 

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

1816 

1817 @property 

1818 def activity_period(self): 

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

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

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

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

1823 

1824 if not self.active: 

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

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

1827 naturaltime(self.deleted_on), deleted_by, 

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

1829 else: 

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

1831 

1832 def __str__(self): 

1833 if self.deleted_on is not None: 

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

1835 else: 

1836 delete_on = "" 

1837 

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

1839 

1840 

1841class Issue(models.Model, UserFiltrableMixin): 

1842 filter_objects_to_db = { 

1843 'filter_description': FilterObjectStr('filters__description', 

1844 'Description of what the filter matches'), 

1845 'filter_runconfig_tag_name': FilterObjectStr('filters__tags__name', 

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

1847 'filter_machine_tag_name': FilterObjectStr('filters__machine_tags__name', 

1848 'Machine tag matched by the filter'), 

1849 'filter_machine_name': FilterObjectStr('filters__machines__name', 

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

1851 'filter_test_name': FilterObjectStr('filters__tests__name', 

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

1853 'filter_status_name': FilterObjectStr('filters__statuses__name', 

1854 'Status matched by the filter'), 

1855 'filter_stdout_regex': FilterObjectStr('filters__stdout_regex', 

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

1857 'filter_stderr_regex': FilterObjectStr('filters__stderr_regex', 

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

1859 'filter_dmesg_regex': FilterObjectStr('filters__dmesg_regex', 

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

1861 'filter_added_on': FilterObjectDateTime('issuefilterassociated__added_on', 

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

1863 'filter_covers_from': FilterObjectDateTime('issuefilterassociated__covers_from', 

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

1865 'filter_deleted_on': FilterObjectDateTime('issuefilterassociated__deleted_on', 

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

1867 'filter_runconfigs_covered_count': FilterObjectInteger('issuefilterassociated__runconfigs_covered_count', 

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

1869 'filter_runconfigs_affected_count': FilterObjectInteger('issuefilterassociated__runconfigs_affected_count', 

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

1871 'filter_last_seen': FilterObjectDateTime('issuefilterassociated__last_seen', 

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

1873 'filter_last_seen_runconfig_name': FilterObjectStr('issuefilterassociated__last_seen_runconfig__name', 

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

1875 

1876 'bug_tracker_name': FilterObjectStr('bugs__tracker__name', 

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

1878 'bug_tracker_short_name': FilterObjectStr('bugs__tracker__short_name', 

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

1880 'bug_tracker_type': FilterObjectStr('bugs__tracker__tracker_type', 

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

1882 'bug_id': FilterObjectStr('bugs__bug_id', 

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

1884 'bug_title': FilterObjectStr('bugs__title', 

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

1886 'bug_created_on': FilterObjectDateTime('bugs__created', 

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

1888 'bug_updated_on': FilterObjectDateTime('bugs__updated', 

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

1890 'bug_closed_on': FilterObjectDateTime('bugs__closed', 

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

1892 'bug_creator_name': FilterObjectStr('bugs__creator__person__full_name', 

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

1894 'bug_creator_email': FilterObjectStr('bugs__creator__person__email', 

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

1896 'bug_assignee_name': FilterObjectStr('bugs__assignee__person__full_name', 

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

1898 'bug_assignee_email': FilterObjectStr('bugs__assignee__person__email', 

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

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

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

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

1903 'bug_features': FilterObjectStr('bugs__features', 

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

1905 'bug_platforms': FilterObjectStr('bugs__platforms', 

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

1907 'bug_status': FilterObjectStr('bugs__status', 

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

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

1910 'bug_tags': FilterObjectStr('bugs__tags', 

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

1912 

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

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

1915 

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

1917 

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

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

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

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

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

1923 'runconfigs_covered_count': FilterObjectInteger('runconfigs_covered_count', 

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

1925 'runconfigs_affected_count': FilterObjectInteger('runconfigs_affected_count', 

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

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

1928 'last_seen_runconfig_name': FilterObjectStr('last_seen_runconfig__name', 

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

1930 } 

1931 

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

1933 bugs = models.ManyToManyField(Bug) 

1934 

1935 description = models.TextField(blank=True) 

1936 filer = models.EmailField() # DEPRECATED 

1937 

1938 added_on = models.DateTimeField(auto_now_add=True) 

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

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

1941 

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

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

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

1945 

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

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

1948 

1949 # Statistics cache 

1950 runconfigs_covered_count = models.PositiveIntegerField(default=0) 

1951 runconfigs_affected_count = models.PositiveIntegerField(default=0) 

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

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

1954 

1955 class Meta: 

1956 permissions = [ 

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

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

1959 

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

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

1962 ] 

1963 

1964 @property 

1965 def archived(self): 

1966 return self.archived_on is not None 

1967 

1968 def hide(self): 

1969 self.expected = True 

1970 self.save() 

1971 

1972 def show(self): 

1973 self.expected = False 

1974 self.save() 

1975 

1976 @cached_property 

1977 def active_filters(self): 

1978 if self.archived: 

1979 deleted_on = self.archived_on 

1980 else: 

1981 deleted_on = None 

1982 

1983 if hasattr(self, 'ifas_cached'): 

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

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

1986 else: 

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

1988 deleted_on=deleted_on) 

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

1990 

1991 @cached_property 

1992 def all_filters(self): 

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

1994 

1995 @property 

1996 def past_filters(self): 

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

1998 

1999 @cached_property 

2000 def bugs_cached(self): 

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

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

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

2004 

2005 @cached_property 

2006 def covers_from(self): 

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

2008 

2009 @cached_property 

2010 def __runfilter_stats_covered__(self): 

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

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

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

2014 covered_count__gt=0, 

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

2016 if self.archived: 

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

2018 

2019 return objs 

2020 

2021 @cached_property 

2022 def runconfigs_covered(self): 

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

2024 

2025 @cached_property 

2026 def runconfigs_affected(self): 

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

2028 # to the set of affected ones 

2029 runconfigs_affected = set() 

2030 for runfilter in self.__runfilter_stats_covered__: 

2031 if runfilter.matched_count > 0: 

2032 runconfigs_affected.add(runfilter.runconfig) 

2033 

2034 return runconfigs_affected 

2035 

2036 def update_statistics(self): 

2037 self.runconfigs_covered_count = len(self.runconfigs_covered) 

2038 self.runconfigs_affected_count = len(self.runconfigs_affected) 

2039 

2040 # Find when the issue was last seen 

2041 for stats in self.__runfilter_stats_covered__: 

2042 if stats.matched_count > 0: 

2043 self.last_seen = stats.runconfig.added_on 

2044 self.last_seen_runconfig = stats.runconfig 

2045 break 

2046 

2047 # Update the statistics atomically in the DB 

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

2049 cur_issue.update(runconfigs_covered_count=self.runconfigs_covered_count, 

2050 runconfigs_affected_count=self.runconfigs_affected_count, 

2051 last_seen=self.last_seen, 

2052 last_seen_runconfig=self.last_seen_runconfig) 

2053 

2054 @property 

2055 def failure_rate(self): 

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

2057 

2058 def matches(self, result): 

2059 if self.archived: 

2060 return False 

2061 

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

2063 if e.filter.matches(result): 

2064 return True 

2065 return False 

2066 

2067 def archive(self, user): 

2068 if self.archived: 

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

2070 

2071 with transaction.atomic(): 

2072 now = timezone.now() 

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

2074 e.delete(user, now) 

2075 self.archived_on = now 

2076 self.archived_by = user 

2077 self.save() 

2078 

2079 # Post a comment 

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

2081 self.comment_on_all_bugs(comment) 

2082 

2083 def restore(self): 

2084 if not self.archived: 

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

2086 

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

2088 with transaction.atomic(): 

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

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

2091 

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

2093 self.archived_on = None 

2094 self.archived_by = None 

2095 self.save() 

2096 

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

2098 self.update_statistics() 

2099 

2100 # Post a comment 

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

2102 self.comment_on_all_bugs(comment) 

2103 

2104 @transaction.atomic 

2105 def set_bugs(self, bugs): 

2106 if self.archived: 

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

2108 

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

2110 self.bugs.clear() 

2111 

2112 for bug in bugs: 

2113 # Make sure the bug exists in the database first 

2114 if bug.id is None: 

2115 bug.save() 

2116 

2117 # Add it to the relation 

2118 self.bugs.add(bug) 

2119 

2120 # Get rid of the cached bugs 

2121 try: 

2122 del self.bugs_cached 

2123 except AttributeError: 

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

2125 pass 

2126 

2127 def _assign_to_known_failures(self, unknown_failures, ifa): 

2128 now = timezone.now() 

2129 new_matched_failures = [] 

2130 for failure in unknown_failures: 

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

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

2133 manually_associated_on=now, 

2134 filing_delay=filing_delay) 

2135 new_matched_failures.append(kf) 

2136 failure.delete() 

2137 

2138 ifa.update_statistics() 

2139 

2140 return new_matched_failures 

2141 

2142 def __filter_add__(self, filter, user): 

2143 # Make sure the filter exists in the database first 

2144 if filter.id is None: 

2145 filter.save() 

2146 

2147 # Create the association between the filter and the issue 

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

2149 

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

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

2152 matched_unknown_failures = ( 

2153 filter.matched_unknown_failures.select_related("result") 

2154 .prefetch_related("result__ts_run") 

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

2156 ) 

2157 return self._assign_to_known_failures(matched_unknown_failures, ifa) 

2158 

2159 def comment_on_all_bugs(self, comment): 

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

2161 

2162 try: 

2163 for bug in self.bugs_cached: 

2164 bug.add_comment(comment) 

2165 except Exception: # pragma: no cover 

2166 traceback.print_exc() # pragma: no cover 

2167 

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

2169 if self.archived: 

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

2171 

2172 with transaction.atomic(): 

2173 # First, add the new filter 

2174 failures = self.__filter_add__(new_filter, user) 

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

2176 

2177 # Delete all active associations of the old filter 

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

2179 for e in assocs: 

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

2181 

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

2183 self.update_statistics() 

2184 

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

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

2187 old_filter.equivalent_user_query != new_filter.equivalent_user_query or 

2188 len(new_matched_failures) > 0): 

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

2190 {"issue": self, "old_filter": old_filter, 

2191 "new_filter": new_filter, "new_matched_failures": new_matched_failures, 

2192 "user": user}) 

2193 self.comment_on_all_bugs(comment) 

2194 

2195 def set_filters(self, filters, user): 

2196 if self.archived: 

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

2198 

2199 with transaction.atomic(): 

2200 removed_ifas = set() 

2201 new_filters = dict() 

2202 

2203 # Query the set of issues that we currently have 

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

2205 

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

2207 now = timezone.now() 

2208 for e in assocs: 

2209 if e.filter not in filters: 

2210 e.delete(user, now) 

2211 removed_ifas.add(e) 

2212 

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

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

2215 for filter in filters: 

2216 if filter.id not in cur_filters_ids: 

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

2218 

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

2220 self.update_statistics() 

2221 

2222 # Get rid of the cached filters 

2223 try: 

2224 del self.active_filters 

2225 except AttributeError: 

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

2227 pass 

2228 

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

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

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

2232 {"issue": self, "removed_ifas": removed_ifas, 

2233 "new_filters": new_filters, "user": user}) 

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

2235 

2236 @transaction.atomic 

2237 def merge_issues(self, issues, user): 

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

2239 

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

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

2242 

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

2244 # archiving them 

2245 for issue in issues: 

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

2247 new_issue_filters.append(filter) 

2248 issue.archive(user) 

2249 

2250 # Set the new list of filters 

2251 self.set_filters(new_issue_filters, user) 

2252 

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

2254 self.update_statistics() 

2255 

2256 def __str__(self): 

2257 bugs = self.bugs.all() 

2258 if len(bugs) == 0: 

2259 return "Issue: <empty>" 

2260 elif len(bugs) == 1: 

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

2262 else: 

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

2264 

2265 

2266class KnownFailure(models.Model, UserFiltrableMixin): 

2267 # For the FilterMixin. 

2268 filter_objects_to_db = { 

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

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

2271 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', 

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

2273 'runconfig_added_on': FilterObjectDateTime('result__ts_run__runconfig__added_on', 

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

2275 'runconfig_temporary': FilterObjectBool('result__ts_run__runconfig__temporary', 

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

2277 'build_name': FilterObjectStr('result__ts_run__runconfig__builds__name', 

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

2279 'build_added_on': FilterObjectDateTime('result__ts_run__runconfig__builds__added_on', 

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

2281 'component_name': FilterObjectStr('result__ts_run__runconfig__builds__component__name', 

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

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

2284 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', 

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

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

2287 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

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

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

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

2291 'manually_filed_on': FilterObjectDateTime('manually_associated_on', 

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

2293 'ifa_id': FilterObjectInteger('matched_ifa_id', 

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

2295 'issue_id': FilterObjectInteger('matched_ifa__issue_id', 

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

2297 'issue_expected': FilterObjectBool('matched_ifa__issue__expected', 

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

2299 'filter_description': FilterObjectStr('matched_ifa__issue__filters__description', 

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

2301 'filter_runconfig_tag_name': 

2302 FilterObjectStr('matched_ifa__issue__filters__tags__name', 

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

2304 'filter_machine_tag_name': FilterObjectStr('matched_ifa__issue__filters__machine_tags__name', 

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

2306 'filter_machine_name': FilterObjectStr('matched_ifa__issue__filters__machines__name', 

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

2308 'filter_test_name': FilterObjectStr('matched_ifa__issue__filters__tests__name', 

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

2310 'filter_status_name': FilterObjectStr('matched_ifa__issue__filters__statuses__name', 

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

2312 'filter_stdout_regex': FilterObjectStr('matched_ifa__issue__filters__stdout_regex', 

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

2314 'filter_stderr_regex': FilterObjectStr('matched_ifa__issue__filters__stderr_regex', 

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

2316 'filter_dmesg_regex': FilterObjectStr('matched_ifa__issue__filters__dmesg_regex', 

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

2318 'filter_added_on': FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__added_on', 

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

2320 'filter_covers_from': 

2321 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__covers_from', 

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

2323 'filter_deleted_on': 

2324 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__deleted_on', 

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

2326 'filter_runconfigs_covered_count': 

2327 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_covered_count', 

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

2329 'filter_runconfigs_affected_count': 

2330 FilterObjectInteger('matched_ifa__issue__issuefilterassociated__runconfigs_affected_count', 

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

2332 'filter_last_seen': 

2333 FilterObjectDateTime('matched_ifa__issue__issuefilterassociated__last_seen', 

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

2335 'filter_last_seen_runconfig_name': 

2336 FilterObjectStr('matched_ifa__issue__issuefilterassociated__last_seen_runconfig__name', 

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

2338 'bug_tracker_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__name', 

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

2340 'bug_tracker_short_name': FilterObjectStr('matched_ifa__issue__bugs__tracker__short_name', 

2341 'Short name of the tracker which holds the bug associated to this failure'), # noqa 

2342 'bug_tracker_type': FilterObjectStr('matched_ifa__issue__bugs__tracker__tracker_type', 

2343 'Type of the tracker which holds the bug associated to this failure'), 

2344 'bug_id': FilterObjectStr('matched_ifa__issue__bugs__bug_id', 

2345 'ID of the bug associated to this failure'), 

2346 'bug_title': FilterObjectStr('matched_ifa__issue__bugs__title', 

2347 'Title of the bug associated to this failure'), 

2348 'bug_created_on': FilterObjectDateTime('matched_ifa__issue__bugs__created', 

2349 'Date at which the bug associated to this failure was created'), 

2350 'bug_closed_on': FilterObjectDateTime('matched_ifa__issue__bugs__closed', 

2351 'Date at which the bug associated to this failure was closed'), 

2352 'bug_creator_name': FilterObjectStr('matched_ifa__issue__bugs__creator__person__full_name', 

2353 'Name of the creator of the bug associated to this failure'), 

2354 'bug_creator_email': FilterObjectStr('matched_ifa__issue__bugs__creator__person__email', 

2355 'Email address of the creator of the bug associated to this failure'), 

2356 'bug_assignee_name': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__full_name', 

2357 'Name of the assignee of the bug associated to this failure'), 

2358 'bug_assignee_email': FilterObjectStr('matched_ifa__issue__bugs__assignee__person__email', 

2359 'Email address of the assignee of the bug associated to this failure'), 

2360 'bug_product': FilterObjectStr('matched_ifa__issue__bugs__product', 

2361 'Product of the bug associated to this failure'), 

2362 'bug_component': FilterObjectStr('matched_ifa__issue__bugs__component', 

2363 'Component of the bug associated to this failure'), 

2364 'bug_priority': FilterObjectStr('matched_ifa__issue__bugs__priority', 

2365 'Priority of the bug associated to this failure'), 

2366 'bug_features': FilterObjectStr('matched_ifa__issue__bugs__features', 

2367 'Features of the bug associated to this failure (coma-separated list)'), 

2368 'bug_platforms': FilterObjectStr('matched_ifa__issue__bugs__platforms', 

2369 'Platforms of the bug associated to this failure (coma-separated list)'), 

2370 'bug_status': FilterObjectStr('matched_ifa__issue__bugs__status', 

2371 'Status of the bug associated to this failure'), 

2372 'bug_tags': FilterObjectStr('matched_ifa__issue__bugs__tags', 

2373 'Tags/labels on the bug associated to this failure (coma-separated list)'), 

2374 'url': FilterObjectStr('result__url', 'External URL of this test result'), 

2375 'start': FilterObjectDateTime('result__start', 'Date at which this test started being executed'), 

2376 'duration': FilterObjectDuration('result__duration', 'Time it took to execute the test'), 

2377 'command': FilterObjectStr('result__command', 'Command used to execute the test'), 

2378 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'), 

2379 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'), 

2380 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'), 

2381 } 

2382 

2383 result = models.ForeignKey(TestResult, on_delete=models.CASCADE, 

2384 related_name="known_failures", related_query_name="known_failure") 

2385 matched_ifa = models.ForeignKey(IssueFilterAssociated, on_delete=models.CASCADE) 

2386 

2387 # When was the mapping done (useful for metrics) 

2388 manually_associated_on = models.DateTimeField(null=True, blank=True, db_index=True) 

2389 filing_delay = models.DurationField(null=True, blank=True) 

2390 

2391 @classmethod 

2392 def _runconfig_index(cls, covered_list, runconfig): 

2393 try: 

2394 covered = sorted(covered_list, key=lambda r: r.added_on, reverse=True) 

2395 return covered.index(runconfig) 

2396 except ValueError: 

2397 return None 

2398 

2399 @cached_property 

2400 def covered_runconfigs_since_for_issue(self): 

2401 return self._runconfig_index(self.matched_ifa.issue.runconfigs_covered, 

2402 self.result.ts_run.runconfig) 

2403 

2404 @cached_property 

2405 def covered_runconfigs_since_for_filter(self): 

2406 return self._runconfig_index(self.matched_ifa.runconfigs_covered, 

2407 self.result.ts_run.runconfig) 

2408 

2409 def __str__(self): 

2410 return "{} associated on {}".format(str(self.result), self.manually_associated_on) 

2411 

2412 

2413class UnknownFailure(models.Model, UserFiltrableMixin): 

2414 filter_objects_to_db = { 

2415 'test_name': FilterObjectStr('result__test__name', "Name of the test"), 

2416 'status_name': FilterObjectStr('result__status__name', "Name of the status of failure"), 

2417 'testsuite_name': FilterObjectStr('result__status__testsuite__name', 

2418 "Name of the testsuite that contains this test"), 

2419 'machine_tag': FilterObjectStr('result__ts_run__machine__tags__name', "Name of the tag associated to machine"), 

2420 'machine_name': FilterObjectStr('result__ts_run__machine__name', "Name of the associated machine"), 

2421 'runconfig_name': FilterObjectStr('result__ts_run__runconfig__name', "Name of the associated runconfig"), 

2422 'runconfig_tag': FilterObjectStr('result__ts_run__runconfig__tags__name', "Tag associated to runconfig"), 

2423 'bug_title': FilterObjectStr('matched_archived_ifas__issue__bugs__description', 

2424 "Description of bug associated to failure"), 

2425 'stdout': FilterObjectStr('result__stdout', 'Standard output of the test execution'), 

2426 'stderr': FilterObjectStr('result__stderr', 'Error output of the test execution'), 

2427 'dmesg': FilterObjectStr('result__dmesg', 'Kernel logs of the test execution'), 

2428 'build_name': FilterObjectStr('result__test__first_runconfig__builds__name', 'Name of the associated build'), 

2429 } 

2430 # We cannot have two UnknownFailure for the same result 

2431 result = models.OneToOneField(TestResult, on_delete=models.CASCADE, 

2432 related_name="unknown_failure") 

2433 

2434 matched_archived_ifas = models.ManyToManyField(IssueFilterAssociated) 

2435 

2436 @cached_property 

2437 def matched_archived_ifas_cached(self): 

2438 return self.matched_archived_ifas.all() 

2439 

2440 @cached_property 

2441 def matched_issues(self): 

2442 issues = set() 

2443 for e in self.matched_archived_ifas_cached: 

2444 issues.add(e.issue) 

2445 return issues 

2446 

2447 def __str__(self): 

2448 return str(self.result) 

2449 

2450 

2451# Allows us to know if a filter covers/matches a runconfig or not 

2452class RunFilterStatistic(models.Model): 

2453 runconfig = models.ForeignKey(RunConfig, on_delete=models.CASCADE) 

2454 filter = models.ForeignKey(IssueFilter, on_delete=models.CASCADE) 

2455 

2456 covered_count = models.PositiveIntegerField() 

2457 matched_count = models.PositiveIntegerField() 

2458 

2459 class Meta: 

2460 constraints = [ 

2461 UniqueConstraint(fields=('runconfig', 'filter'), name='unique_runconfig_filter') 

2462 ] 

2463 

2464 def __str__(self): 

2465 if self.covered_count > 0: 

2466 perc = self.matched_count * 100 / self.covered_count 

2467 else: 

2468 perc = 0 

2469 return "{} on {}: match rate {}/{} ({:.2f}%)".format(self.filter, 

2470 self.runconfig, 

2471 self.matched_count, 

2472 self.covered_count, 

2473 perc)