Coverage for CIResults / serializers.py: 89%

431 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-04 08:33 +0000

1import datetime 

2from django.db import transaction 

3from collections import namedtuple, OrderedDict 

4 

5from django.db.models import QuerySet 

6from rest_framework import serializers 

7 

8from .run_import import RunConfigResults, ResultsCommitHandler, TestsuiteResults, TestsuiteTestResult 

9from .models import Bug, Component, Build, Test, Machine, RunConfigTag, RunConfig, ReplicationScript, UnknownFailure 

10from .models import TestSuite, TextStatus, IssueFilter, MachineTag, BugTrackerAccount 

11from .models import Issue, BugTracker 

12 

13from shortener.models import Shortener 

14from drf_spectacular.utils import extend_schema_field 

15from drf_spectacular.types import OpenApiTypes 

16 

17 

18@extend_schema_field(OpenApiTypes.NUMBER) 

19class TimedeltaField(serializers.Field): 

20 def to_internal_value(self, data): 

21 try: 

22 if isinstance(data, datetime.timedelta): 

23 return data 

24 if isinstance(data, int) or isinstance(data, float): 

25 return datetime.timedelta(seconds=data) 

26 if data is None: 

27 return datetime.timedelta() 

28 except (ValueError, TypeError): 

29 pass 

30 raise serializers.ValidationError("A valid number or timedelta is required.") 

31 

32 def to_representation(self, value): 

33 return value.total_seconds() 

34 

35 

36class UpdateMixin(serializers.ModelSerializer): 

37 def get_extra_kwargs(self): 

38 kwargs = super().get_extra_kwargs() 

39 no_update_fields = getattr(self.Meta, "no_update_fields", None) 

40 

41 if self.instance and no_update_fields: 

42 for field in no_update_fields: 

43 kwargs.setdefault(field, {}) 

44 kwargs[field]["read_only"] = True 

45 

46 return kwargs 

47 

48 

49class DynamicFieldsModelSerializer(serializers.ModelSerializer): 

50 """ 

51 A ModelSerializer that takes an additional `extra_fields` argument that 

52 controls which fields should be displayed. Subclass should define a 

53 default_fields list. 

54 """ 

55 default_fields = [] 

56 

57 def __init__(self, *args, **kwargs): 

58 extra_fields_to_serialize = kwargs.pop('extra_fields', None) 

59 super().__init__(*args, **kwargs) 

60 

61 if extra_fields_to_serialize is not None: 

62 # Drop extra fields that are not specified in the `extra_fields` 

63 # argument. 

64 default = set(self.default_fields) 

65 existing = set(self.fields) 

66 for field_name in existing - default - set(extra_fields_to_serialize): 

67 self.fields.pop(field_name) 

68 

69 @classmethod 

70 def extra_fields(cls) -> list[str]: 

71 return list(set(cls.Meta.fields) - set(cls.default_fields)) 

72 

73 

74class RunConfigTagSerializer(serializers.ModelSerializer): 

75 class Meta: 

76 model = RunConfigTag 

77 fields = ('id', 'description', 'url', 'public', '__str__') 

78 read_only_fields = ('public', ) 

79 

80 

81class TestSuiteSerializer(serializers.ModelSerializer): 

82 class Meta: 

83 model = TestSuite 

84 fields = ('id', '__str__') 

85 

86 

87class TestSerializer(serializers.ModelSerializer): 

88 testsuite = TestSuiteSerializer() 

89 

90 class Meta: 

91 model = Test 

92 fields = ('id', 'name', 'testsuite', 'public', 'vetted_on', 'first_runconfig', '__str__') 

93 read_only_fields = ('public', ) 

94 

95 

96class MachineTagSerializer(serializers.ModelSerializer): 

97 class Meta: 

98 model = MachineTag 

99 fields = ('id', 'name', 'public') 

100 read_only_fields = ('added_on', ) 

101 

102 

103class MachineSerializer(serializers.ModelSerializer): 

104 class Meta: 

105 model = Machine 

106 fields = ('id', 'public', 'vetted_on', '__str__') 

107 read_only_fields = ('public', ) 

108 

109 

110class ImportMachineSerializer(serializers.ModelSerializer): 

111 tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=MachineTag.objects.all(), default=[]) 

112 aliases = serializers.SlugRelatedField( 

113 slug_field='name', queryset=Machine.objects.all(), required=False, allow_null=True, 

114 ) 

115 vetted = serializers.BooleanField(source="is_vetted_on", required=False, write_only=True) 

116 

117 class Meta: 

118 model = Machine 

119 fields = ('id', 'name', 'description', 'public', 'aliases', 'tags', "vetted", "vetted_on") 

120 read_only_fields = ("vetted_on",) 

121 extra_kwargs = { 

122 "public": {"default": False}, 

123 } 

124 

125 def to_internal_value(self, data): 

126 # HACK: Remove "vetted" field from the data, because the model has a read-only property `vetted`, 

127 # which fails the serializer's validation 

128 if data.get('vetted') is not None: 

129 self.context["vetted"] = data.pop("vetted") 

130 else: 

131 self.context["vetted"] = False 

132 if data.get("tags"): 

133 machine_tags_db = set(MachineTag.objects.all().values_list("name", flat=True)) 

134 machine_tags = set(data["tags"]) 

135 # Get machine tags that doesn't exist in the database and insert them into context dictionary 

136 new_tags_names = machine_tags - machine_tags_db 

137 self.context["new_tags"] = [MachineTag(name=new_tag, public=True) for new_tag in new_tags_names] 

138 # Leave only names of machine tags that exist in the database so that they pass the serializer's validation 

139 data["tags"] = machine_tags & machine_tags_db 

140 return super().to_internal_value(data) 

141 

142 @transaction.atomic 

143 def create(self, validated_data): 

144 machine = super().create(validated_data) 

145 if new_tags := self.context.get("new_tags"): 

146 new_tags = MachineTag.objects.bulk_create(self.context["new_tags"]) 

147 machine.tags.add(*new_tags) 

148 if self.context.get("vetted"): 

149 machine.vet() 

150 machine.save() 

151 return machine 

152 

153 

154class TextStatusSerializer(serializers.ModelSerializer): 

155 testsuite = TestSuiteSerializer() 

156 

157 class Meta: 

158 model = TextStatus 

159 fields = ('id', 'name', 'testsuite', '__str__') 

160 

161 

162class IssueFilterSerializer(serializers.ModelSerializer): 

163 tags = RunConfigTagSerializer(many=True) 

164 tests = TestSerializer(many=True) 

165 machine_tags = MachineTagSerializer(many=True) 

166 machines = MachineSerializer(many=True) 

167 statuses = TextStatusSerializer(many=True) 

168 

169 class Meta: 

170 model = IssueFilter 

171 fields = ('id', 'description', 'tags', 'machines', 'machine_tags', 'tests', 'statuses', 

172 'stdout_regex', 'stderr_regex', 'dmesg_regex', 'user_query', '__str__') 

173 read_only_fields = ('added_on', ) 

174 

175 

176class RunConfigSerializer(serializers.ModelSerializer): 

177 tags = serializers.SlugRelatedField(many=True, slug_field='name', queryset=RunConfigTag.objects.all()) 

178 builds = serializers.SlugRelatedField(many=True, slug_field='name', queryset=Build.objects.all()) 

179 

180 class Meta: 

181 model = RunConfig 

182 fields = ('id', 'name', 'tags', 'url', 'added_on', 'builds', 'environment', 'temporary', '__str__') 

183 read_only_fields = ('added_on', ) 

184 

185 def validate(self, data): 

186 components = {} 

187 for build in data["builds"]: 

188 if build.component not in components: 

189 components[build.component] = build 

190 else: 

191 raise serializers.ValidationError( 

192 f"Two builds ({components[build.component]} and {build}) cannot be from the same component" 

193 ) 

194 return data 

195 

196 

197class ImportTestResultSerializer(serializers.Serializer): 

198 id = serializers.IntegerField(read_only=True) 

199 status = serializers.CharField() 

200 duration = TimedeltaField(required=False, default=datetime.timedelta()) 

201 command = serializers.CharField(required=False, default="") 

202 stdout = serializers.CharField(required=False, allow_null=True, write_only=True) 

203 stderr = serializers.CharField(required=False, allow_null=True, write_only=True) 

204 dmesg = serializers.CharField(required=False, allow_null=True, write_only=True) 

205 url = serializers.URLField(required=False, allow_null=True) 

206 start = serializers.DateTimeField(required=False, default=datetime.datetime.now()) 

207 test_name = serializers.CharField() 

208 

209 def to_representation(self, instance): 

210 self.fields.pop("test_name", None) 

211 representation = super().to_representation(instance) 

212 representation["test_name"] = instance.test.name 

213 return representation 

214 

215 

216class ImportTestSuiteRunSerializer(serializers.Serializer): 

217 runconfig_name = serializers.CharField(required=True, write_only=True) 

218 runconfig = serializers.SerializerMethodField(read_only=True) 

219 test_results = serializers.DictField( 

220 child=serializers.DictField( 

221 child=serializers.ListField( 

222 child=ImportTestResultSerializer(), 

223 required=True, 

224 ), 

225 required=True, 

226 ), 

227 required=True, 

228 ) 

229 test_suite_name = serializers.CharField(required=True, write_only=True) 

230 test_suite = serializers.SerializerMethodField(read_only=True) 

231 

232 @extend_schema_field({ 

233 "type": "object", 

234 "properties": {"id": {"type": "integer"}, "name": {"type": "string"}} 

235 }) 

236 def get_runconfig(self, obj): 

237 runconfig = obj["runconfig"] 

238 return {"id": runconfig.id, "name": runconfig.name} 

239 

240 @extend_schema_field({ 

241 "type": "object", 

242 "properties": {"id": {"type": "integer"}, "name": {"type": "string"}} 

243 }) 

244 def get_test_suite(self, obj): 

245 test_suite = obj["test_suite"] 

246 return {"id": test_suite.id, "name": test_suite.name} 

247 

248 @transaction.atomic 

249 def create(self, validated_data): 

250 run_results = [] 

251 try: 

252 test_suite = Build.objects.get(name=validated_data["test_suite_name"]) 

253 test_suite_results = TestsuiteResults( 

254 runconfig=None, 

255 name=test_suite.component.name, 

256 build=test_suite, 

257 result_url_pattern=validated_data.get("result_url_pattern", ""), 

258 format="json", 

259 format_version=None, 

260 ) 

261 except Build.DoesNotExist: 

262 raise ValueError(f"Testsuite build {validated_data['test_suite_name']} does not exist") 

263 for machine_name, raw_machine in validated_data.get("test_results").items(): 

264 for run_id, raw_run in raw_machine.items(): 

265 testsuite_test_results = [] 

266 for result in raw_run: 

267 testsuite_test_results.append(TestsuiteTestResult( 

268 name=result["test_name"], 

269 status=result["status"], 

270 start_time=result["start"], 

271 duration=result.get("duration"), 

272 command=result.get("command"), 

273 stdout=result.get("stdout"), 

274 stderr=result.get("stderr"), 

275 dmesg=result.get("dmesg"), 

276 url=result.get("url") 

277 )) 

278 run_results.append(test_suite_results.read_results(machine_name, int(run_id), testsuite_test_results)) 

279 

280 rc = RunConfigResults(name=validated_data["runconfig_name"], run_results=run_results) 

281 commit_handler = ResultsCommitHandler(rc) 

282 commit_handler.commit() 

283 return { 

284 "runconfig": commit_handler.runconfig, 

285 "test_results": commit_handler.test_results_by_machine_and_run, 

286 "test_suite": test_suite, 

287 } 

288 

289 

290class MinimalMachineSerializer(serializers.ModelSerializer): 

291 tags = serializers.StringRelatedField(many=True) 

292 

293 class Meta: 

294 model = Machine 

295 fields = ['name', 'tags'] 

296 

297 

298class MinimalRunConfigSerializer(serializers.ModelSerializer): 

299 tags = serializers.StringRelatedField(many=True) 

300 

301 class Meta: 

302 model = RunConfig 

303 fields = ['name', 'tags'] 

304 

305 

306class UnknownFailureSerializer(DynamicFieldsModelSerializer): 

307 test = serializers.StringRelatedField(source="result.test.name") 

308 status = serializers.StringRelatedField(source="result.status.name") 

309 dmesg = serializers.StringRelatedField(source="result.dmesg") 

310 stdout = serializers.StringRelatedField(source="result.stdout") 

311 stderr = serializers.StringRelatedField(source="result.stderr") 

312 runconfig = MinimalRunConfigSerializer(source="result.ts_run.runconfig") 

313 machine = MinimalMachineSerializer(source="result.ts_run.machine") 

314 testsuite = serializers.StringRelatedField(source="result.ts_run.testsuite.name") 

315 default_fields = ["id", "test", "status", "runconfig", "machine", "testsuite"] 

316 

317 class Meta: 

318 model = UnknownFailure 

319 fields = ["id", "test", "status", "dmesg", "stdout", "stderr", "runconfig", "machine", "testsuite"] 

320 

321 

322class ComponentSerializer(serializers.ModelSerializer): 

323 class Meta: 

324 model = Component 

325 fields = ('id', 'name', 'description', 'url', 'public', '__str__') 

326 

327 

328class BuildSerializer(UpdateMixin, serializers.ModelSerializer): 

329 component = serializers.SlugRelatedField(slug_field='name', queryset=Component.objects.all()) 

330 parents = serializers.SlugRelatedField(many=True, slug_field='name', queryset=Build.objects.all()) 

331 

332 class Meta: 

333 model = Build 

334 fields = ('id', 'name', 'component', 'version', 'added_on', 'parents', 

335 'repo_type', 'branch', 'repo', 'upstream_url', 'parameters', 

336 'build_log', '__str__') 

337 read_only_fields = ('id', 'added_on') 

338 no_update_fields = ['name', 'component'] 

339 

340 

341class BuildMinimalSerializer(serializers.ModelSerializer): 

342 class Meta: 

343 model = Build 

344 fields = ('id', 'name', 'added_on', 'parents', 'upstream_url', '__str__') 

345 read_only_fields = ('id', 'added_on') 

346 

347 

348class RunConfigResultsSerializer(serializers.Serializer): 

349 __str__ = serializers.CharField(max_length=255, read_only=True) 

350 is_failure = serializers.BooleanField(read_only=True) 

351 all_failures_covered = serializers.BooleanField(read_only=True) 

352 bugs_covering = serializers.SerializerMethodField() 

353 

354 def get_bugs_covering(self, obj): 

355 ser = serializers.ListField(child=serializers.CharField(max_length=255, read_only=True)) 

356 return ser.to_representation([b.short_name for b in obj.bugs_covering]) 

357 

358 

359class RunConfigResultsDiffSerializer(serializers.Serializer): 

360 testsuite = serializers.SerializerMethodField() 

361 test = serializers.SerializerMethodField() 

362 machine = serializers.SerializerMethodField() 

363 result_from = RunConfigResultsSerializer(read_only=True) 

364 result_to = RunConfigResultsSerializer(read_only=True) 

365 

366 def get_testsuite(self, obj): 

367 ser = serializers.CharField(max_length=255, read_only=True) 

368 return ser.to_representation(obj.testsuite.name) 

369 

370 def get_test(self, obj): 

371 ser = serializers.CharField(max_length=255, read_only=True) 

372 return ser.to_representation(obj.test.name) 

373 

374 def get_machine(self, obj): 

375 ser = serializers.CharField(max_length=255, read_only=True) 

376 return ser.to_representation(obj.machine.name) 

377 

378 

379class BugTrackerSerializer(serializers.ModelSerializer): 

380 class Meta: 

381 model = BugTracker 

382 fields = ('id', 'name', 'short_name', 'project', 'separator', 'url', 'tracker_type', 'polled', 

383 'components_followed', 'components_followed_since', 'first_response_SLA') 

384 read_only_fields = ('id', 'name', 'short_name', 'project', 'separator', 'url', 'tracker_type', 'polled', 

385 'components_followed', 'components_followed_since', 'first_response_SLA') 

386 

387 

388def serialize_bug(bug, new_comments=None): 

389 def _date_formatter(date_field): 

390 return str(date_field) if date_field is not None else None 

391 

392 resp = { 

393 'url': bug.url, 

394 'bug_id': bug.bug_id, 

395 'title': bug.title, 

396 'description': bug.description, 

397 'tracker': str(bug.tracker), 

398 'created': _date_formatter(bug.created), 

399 'updated': _date_formatter(bug.updated), 

400 'polled': _date_formatter(bug.polled), 

401 'closed': _date_formatter(bug.closed), 

402 'creator': str(bug.creator), 

403 'assignee': str(bug.assignee), 

404 'product': bug.product, 

405 'component': bug.component, 

406 'priority': bug.priority, 

407 'severity': bug.severity, 

408 'features': bug.features_list, 

409 'platforms': bug.platforms_list, 

410 'status': bug.status, 

411 'tags': bug.tags_list, 

412 'custom_fields': bug.custom_fields, 

413 'new_comments': [] 

414 } 

415 if new_comments: 

416 for comm in new_comments: 

417 person = comm.db_object.account.person 

418 author = person.full_name if person.full_name else person.email 

419 resp['new_comments'].append({'author': author, 

420 'created': str(comm.db_object.created_on), 

421 'body': comm.body}) 

422 return resp 

423 

424 

425class BugCompleteSerializer(serializers.ModelSerializer): 

426 tracker = BugTrackerSerializer() 

427 

428 class Meta: 

429 model = Bug 

430 fields = ('id', 'tracker', 'bug_id', 'parent', 'children', 'title', 'description', 'created', 

431 'updated', 'polled', 'closed', 'creator', 'assignee', 'product', 'component', 

432 'priority', 'severity', 'features', 'platforms', 'status', 'tags', 'comments_polled', 

433 'flagged_as_update_pending_on', 'custom_fields') 

434 read_only_fields = ('id', 'tracker', 'bug_id', 'parent', 'children', 'title', 'description', 'created', 

435 'updated', 'polled', 'closed', 'creator', 'assignee', 'product', 'component', 

436 'priority', 'severity', 'features', 'platforms', 'status', 'tags', 'comments_polled', 

437 'flagged_as_update_pending_on', 'custom_fields') 

438 

439 

440class BugMinimalSerializer(serializers.ModelSerializer): 

441 class Meta: 

442 model = Bug 

443 fields = ('id', 'short_name', 'url') 

444 read_only_fields = ('id', 'short_name', 'url') 

445 

446 

447class ExecutionTimeSerializer(serializers.Serializer): 

448 minimum = serializers.DurationField(read_only=True) 

449 maximum = serializers.DurationField(read_only=True) 

450 count = serializers.IntegerField(read_only=True) 

451 

452 

453class RunConfigDiffSerializer(serializers.Serializer): 

454 runcfg_from = RunConfigSerializer(read_only=True) 

455 runcfg_to = RunConfigSerializer(read_only=True) 

456 results = RunConfigResultsDiffSerializer(read_only=True, many=True) 

457 new_tests = serializers.SerializerMethodField() 

458 builds = serializers.SerializerMethodField() 

459 bugs = BugMinimalSerializer(many=True) 

460 status = serializers.CharField(max_length=10, read_only=True) 

461 text = serializers.CharField(max_length=16000, read_only=True) 

462 

463 class BuildDiff2Serializer(serializers.Serializer): 

464 component = ComponentSerializer(read_only=True) 

465 from_build = BuildMinimalSerializer(read_only=True) 

466 to_build = BuildMinimalSerializer(read_only=True) 

467 

468 class RunConfigDiffNewTestsSerializer(serializers.Serializer): 

469 test = TestSerializer() 

470 statuses = serializers.DictField(child=serializers.IntegerField(read_only=True)) 

471 exec_time = ExecutionTimeSerializer(read_only=True) 

472 

473 @extend_schema_field(serializers.DictField(child=BuildDiff2Serializer())) 

474 def get_builds(self, obj): 

475 bd2 = namedtuple('BuildDiff2', ['component', 'from_build', 'to_build']) 

476 build_diffs = {k: bd2(k, v.from_build, v.to_build) for k, v in obj.builds.items()} 

477 dict_ser = serializers.DictField(child=self.BuildDiff2Serializer()) 

478 return dict_ser.to_representation(build_diffs) 

479 

480 def __statuses(self, statuses): 

481 return {k.name: v for k, v in statuses.items()} 

482 

483 @extend_schema_field(serializers.DictField(child=RunConfigDiffNewTestsSerializer())) 

484 def get_new_tests(self, obj): 

485 NT = namedtuple('NewTest', ['test', 'statuses', 'exec_time']) 

486 new_tests = {k: NT(k, {k.name: v for k, v in v.to_statuses.items()}, v.to_exec_times) 

487 for k, v in obj.new_tests.tests.items()} 

488 dict_ser = serializers.DictField(child=self.RunConfigDiffNewTestsSerializer()) 

489 return dict_ser.to_representation(new_tests) 

490 

491 

492class ReplicationScriptSerializer(serializers.ModelSerializer): 

493 created_by = serializers.StringRelatedField() 

494 source_tracker = serializers.StringRelatedField() 

495 destination_tracker = serializers.StringRelatedField() 

496 

497 class Meta: 

498 model = ReplicationScript 

499 fields = ('name', 'created_by', 'created_on', 'source_tracker', 'destination_tracker', 'script') 

500 

501 

502class KnownIssuesSerializer(serializers.Serializer): 

503 id = serializers.IntegerField(read_only=True) 

504 testsuite = serializers.SerializerMethodField() 

505 machine = serializers.SerializerMethodField() 

506 run_id = serializers.SerializerMethodField() 

507 test = serializers.SerializerMethodField() 

508 status = serializers.SerializerMethodField() 

509 url = serializers.SerializerMethodField() 

510 

511 bugs = serializers.SerializerMethodField() 

512 

513 def __init__(self, *args, **kwargs): 

514 super().__init__(*args, **kwargs) 

515 

516 # Cache the serializers for performance reasons 

517 self._char_ser = serializers.CharField(max_length=255, read_only=True) 

518 self._int_ser = serializers.IntegerField(read_only=True) 

519 self._bug_min_ser = BugMinimalSerializer(many=True) 

520 

521 @extend_schema_field(OpenApiTypes.INT) 

522 def get_run_id(self, obj): 

523 return self._int_ser.to_representation(obj.result.ts_run.run_id) 

524 

525 @extend_schema_field(OpenApiTypes.STR) 

526 def get_testsuite(self, obj): 

527 return self._char_ser.to_representation(obj.result.test.testsuite.name) 

528 

529 @extend_schema_field(OpenApiTypes.STR) 

530 def get_test(self, obj): 

531 return self._char_ser.to_representation(obj.result.test.name) 

532 

533 @extend_schema_field(OpenApiTypes.STR) 

534 def get_machine(self, obj): 

535 return self._char_ser.to_representation(obj.result.ts_run.machine.name) 

536 

537 @extend_schema_field(OpenApiTypes.STR) 

538 def get_status(self, obj): 

539 return self._char_ser.to_representation(obj.result.status.name) 

540 

541 @extend_schema_field(OpenApiTypes.STR) 

542 def get_url(self, obj): 

543 return self._char_ser.to_representation(obj.result.url) 

544 

545 @extend_schema_field(BugMinimalSerializer) 

546 def get_bugs(self, obj): 

547 return self._bug_min_ser.to_representation(obj.matched_ifa.issue.bugs.all()) 

548 

549 

550class BugTrackerAccountSerializer(serializers.ModelSerializer): 

551 class Meta: 

552 model = BugTrackerAccount 

553 fields = ('id', 'is_developer') 

554 read_only_fields = ('id', ) 

555 

556 

557class ShortenerSerializer(serializers.ModelSerializer): 

558 class Meta: 

559 model = Shortener 

560 fields = ('id', 'shorthand', 'full', 'added_on', 'last_accessed') 

561 read_only_fields = ('id', 'shorthand', 'full', 'added_on', 'last_accessed') 

562 

563 

564class RateSerializer(serializers.Serializer): 

565 count = serializers.IntegerField() 

566 total = serializers.IntegerField() 

567 percent = serializers.FloatField() 

568 

569 

570class BugSerializer(serializers.ModelSerializer): 

571 class Meta: 

572 model = Bug 

573 fields = ('short_name', 'title', 'url') 

574 read_only_fields = ('short_name', 'title', 'url') 

575 

576 

577class RestIssueSerializer(serializers.ModelSerializer): 

578 class Meta: 

579 model = Issue 

580 fields = ('__all__') 

581 read_only_fields = ('id', 'added_on', 'archived_on', 'runconfigs_covered_count', 'runconfigs_affected_count', 

582 'last_seen', 'added_by', 'archived_by', 'last_seen_runconfig') 

583 

584 bugs = BugSerializer(many=True) 

585 

586 

587class IssueSerializer(serializers.ModelSerializer): 

588 class Meta: 

589 model = Issue 

590 fields = ('id', 'bugs', 'expected') 

591 read_only_fields = ('id', 'bugs', 'expected') 

592 

593 bugs = BugSerializer(many=True) 

594 

595 

596class IssueSerializerMinimal(serializers.ModelSerializer): 

597 class Meta: 

598 model = Issue 

599 fields = ('id', ) 

600 read_only_fields = ('id', ) 

601 

602 

603def serialize_issue_hitrate(issues, minimal=False): 

604 Serializer = IssueSerializerMinimal if minimal else IssueSerializer 

605 

606 ret = [] 

607 for issue, rate in issues.items(): 

608 val = Serializer(issue).data 

609 val['hit_rate'] = RateSerializer(rate).data 

610 ret.append(val) 

611 return ret 

612 

613 

614def serialize_MetricPassRatePerRunconfig(history): 

615 runconfigs = OrderedDict() 

616 for runconfig, _statuses in history.runconfigs.items(): 

617 runconfigs[str(runconfig)] = OrderedDict() 

618 for status, rate in _statuses.items(): 

619 runconfigs[str(runconfig)][str(status)] = RateSerializer(rate).data 

620 

621 statuses = OrderedDict() 

622 for status, _runconfigs in history.statuses.items(): 

623 statuses[str(status)] = OrderedDict() 

624 for runconfig, rate in _runconfigs.items(): 

625 statuses[str(status)][str(runconfig)] = RateSerializer(rate).data 

626 

627 return { 

628 "runconfigs": runconfigs, 

629 "statuses": statuses, 

630 "most_hit_issues": serialize_issue_hitrate(history.most_hit_issues), 

631 "query_key": history.query.query_key, 

632 } 

633 

634 

635class IssueAddFilterSerializer(serializers.Serializer): 

636 filters = serializers.ListField( 

637 child=serializers.IntegerField(), 

638 allow_empty=False, 

639 write_only=True, 

640 ) 

641 

642 def to_internal_value(self, data: dict) -> dict: 

643 internal: dict = super().to_internal_value(data) 

644 filters: QuerySet[IssueFilter] = IssueFilter.objects.filter(id__in=internal["filters"]) 

645 found_ids: set[int] = set(filters.values_list("id", flat=True)) 

646 requested_ids: set[int] = set(internal["filters"]) 

647 missing_ids: set[int] = requested_ids - found_ids 

648 

649 if missing_ids: 

650 raise serializers.ValidationError( 

651 f"IssueFilter objects with IDs {sorted(missing_ids)} do not exist" 

652 ) 

653 internal["filters"] = filters 

654 return internal 

655 

656 

657class PassRateStatisticsSerializer(serializers.Serializer): 

658 passrate = RateSerializer() 

659 runrate = RateSerializer() 

660 discarded_rate = RateSerializer() 

661 notrun_rate = RateSerializer() 

662 

663 

664def serialize_MetricPassRatePerTest(metric_passrate): 

665 discarded_status = "discarded (expected)" 

666 

667 tests = OrderedDict() 

668 for test, results in metric_passrate.tests.items(): 

669 if results.is_fully_discarded: 

670 tests[str(test)] = { 

671 "status": discarded_status, 

672 "is_pass": False, 

673 "is_run": False, 

674 "duration": str(results.duration), 

675 "issues_hit": serialize_issue_hitrate(results.issue_occurence_rates, minimal=True), 

676 } 

677 else: 

678 tests[str(test)] = { 

679 "status": str(results.overall_result), 

680 "is_pass": results.is_pass, 

681 "is_run": not results.overall_result.is_notrun, 

682 "duration": str(results.duration), 

683 "issues_hit": serialize_issue_hitrate(results.issue_occurence_rates, minimal=True), 

684 } 

685 

686 statuses = OrderedDict() 

687 for status, rate in metric_passrate.statuses.items(): 

688 statuses[str(status)] = RateSerializer(rate).data 

689 

690 return { 

691 "tests": tests, 

692 "statuses": statuses, 

693 "discarded_status": discarded_status, 

694 "machines": [str(m) for m in metric_passrate.machines], 

695 "runconfigs": RunConfigSerializer(metric_passrate.runconfigs, many=True).data, 

696 "raw_statistics": PassRateStatisticsSerializer(metric_passrate.raw_statistics).data, 

697 "statistics": PassRateStatisticsSerializer(metric_passrate.statistics).data, 

698 "most_hit_issues": serialize_issue_hitrate(metric_passrate.most_hit_issues), 

699 "uncovered_failure_rate": RateSerializer(metric_passrate.uncovered_failure_rate).data, 

700 "notrun_rate": RateSerializer(metric_passrate.notrun_rate).data, 

701 "most_interrupting_issues": serialize_issue_hitrate(metric_passrate.most_interrupting_issues), 

702 "unknown_failure_interruption_rate": RateSerializer(metric_passrate.unknown_failure_interruption_rate).data, 

703 "unexplained_interruption_rate": RateSerializer(metric_passrate.unexplained_interruption_rate).data, 

704 "query_key": metric_passrate.query.query_key, 

705 }