Coverage for CIResults/serializers.py: 89%

444 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-13 07:21 +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 TrackerFromContextDefault: 

448 requires_context = True 

449 

450 def __call__(self, serializer_field): 

451 return serializer_field.context['view'].bugtracker 

452 

453 

454class BugDetailSerializer(UpdateMixin, serializers.ModelSerializer): 

455 tracker = BugTrackerSerializer(read_only=True, default=TrackerFromContextDefault()) 

456 assignee = serializers.SlugRelatedField( 

457 slug_field='user_id', queryset=BugTrackerAccount.objects.all(), allow_null=True, required=False 

458 ) 

459 creator = serializers.SlugRelatedField( 

460 slug_field='user_id', queryset=BugTrackerAccount.objects.all(), allow_null=True, required=False 

461 ) 

462 

463 class Meta: 

464 model = Bug 

465 fields = ('id', 'bug_id', 'tracker', 'title', 'description', 'created', 

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

467 'priority', 'severity', 'features', 'platforms', 'status', 'tags', 'parent', 

468 'children', 'comments_polled') 

469 read_only_fields = ('id', 'created', 'updated', 'polled', 'closed', 'children') 

470 no_update_fields = ('bug_id', 'creator', 'parent') 

471 

472 

473class ExecutionTimeSerializer(serializers.Serializer): 

474 minimum = serializers.DurationField(read_only=True) 

475 maximum = serializers.DurationField(read_only=True) 

476 count = serializers.IntegerField(read_only=True) 

477 

478 

479class RunConfigDiffSerializer(serializers.Serializer): 

480 runcfg_from = RunConfigSerializer(read_only=True) 

481 runcfg_to = RunConfigSerializer(read_only=True) 

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

483 new_tests = serializers.SerializerMethodField() 

484 builds = serializers.SerializerMethodField() 

485 bugs = BugMinimalSerializer(many=True) 

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

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

488 

489 class BuildDiff2Serializer(serializers.Serializer): 

490 component = ComponentSerializer(read_only=True) 

491 from_build = BuildMinimalSerializer(read_only=True) 

492 to_build = BuildMinimalSerializer(read_only=True) 

493 

494 class RunConfigDiffNewTestsSerializer(serializers.Serializer): 

495 test = TestSerializer() 

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

497 exec_time = ExecutionTimeSerializer(read_only=True) 

498 

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

500 def get_builds(self, obj): 

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

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

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

504 return dict_ser.to_representation(build_diffs) 

505 

506 def __statuses(self, statuses): 

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

508 

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

510 def get_new_tests(self, obj): 

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

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

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

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

515 return dict_ser.to_representation(new_tests) 

516 

517 

518class ReplicationScriptSerializer(serializers.ModelSerializer): 

519 created_by = serializers.StringRelatedField() 

520 source_tracker = serializers.StringRelatedField() 

521 destination_tracker = serializers.StringRelatedField() 

522 

523 class Meta: 

524 model = ReplicationScript 

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

526 

527 

528class KnownIssuesSerializer(serializers.Serializer): 

529 id = serializers.IntegerField(read_only=True) 

530 testsuite = serializers.SerializerMethodField() 

531 machine = serializers.SerializerMethodField() 

532 run_id = serializers.SerializerMethodField() 

533 test = serializers.SerializerMethodField() 

534 status = serializers.SerializerMethodField() 

535 url = serializers.SerializerMethodField() 

536 

537 bugs = serializers.SerializerMethodField() 

538 

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

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

541 

542 # Cache the serializers for performance reasons 

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

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

545 self._bug_min_ser = BugMinimalSerializer(many=True) 

546 

547 @extend_schema_field(OpenApiTypes.INT) 

548 def get_run_id(self, obj): 

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

550 

551 @extend_schema_field(OpenApiTypes.STR) 

552 def get_testsuite(self, obj): 

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

554 

555 @extend_schema_field(OpenApiTypes.STR) 

556 def get_test(self, obj): 

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

558 

559 @extend_schema_field(OpenApiTypes.STR) 

560 def get_machine(self, obj): 

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

562 

563 @extend_schema_field(OpenApiTypes.STR) 

564 def get_status(self, obj): 

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

566 

567 @extend_schema_field(OpenApiTypes.STR) 

568 def get_url(self, obj): 

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

570 

571 @extend_schema_field(BugMinimalSerializer) 

572 def get_bugs(self, obj): 

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

574 

575 

576class BugTrackerAccountSerializer(serializers.ModelSerializer): 

577 class Meta: 

578 model = BugTrackerAccount 

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

580 read_only_fields = ('id', ) 

581 

582 

583class ShortenerSerializer(serializers.ModelSerializer): 

584 class Meta: 

585 model = Shortener 

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

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

588 

589 

590class RateSerializer(serializers.Serializer): 

591 count = serializers.IntegerField() 

592 total = serializers.IntegerField() 

593 percent = serializers.FloatField() 

594 

595 

596class BugSerializer(serializers.ModelSerializer): 

597 class Meta: 

598 model = Bug 

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

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

601 

602 

603class RestIssueSerializer(serializers.ModelSerializer): 

604 class Meta: 

605 model = Issue 

606 fields = ('__all__') 

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

608 'last_seen', 'added_by', 'archived_by', 'last_seen_runconfig') 

609 

610 bugs = BugSerializer(many=True) 

611 

612 

613class IssueSerializer(serializers.ModelSerializer): 

614 class Meta: 

615 model = Issue 

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

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

618 

619 bugs = BugSerializer(many=True) 

620 

621 

622class IssueSerializerMinimal(serializers.ModelSerializer): 

623 class Meta: 

624 model = Issue 

625 fields = ('id', ) 

626 read_only_fields = ('id', ) 

627 

628 

629def serialize_issue_hitrate(issues, minimal=False): 

630 Serializer = IssueSerializerMinimal if minimal else IssueSerializer 

631 

632 ret = [] 

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

634 val = Serializer(issue).data 

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

636 ret.append(val) 

637 return ret 

638 

639 

640def serialize_MetricPassRatePerRunconfig(history): 

641 runconfigs = OrderedDict() 

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

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

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

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

646 

647 statuses = OrderedDict() 

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

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

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

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

652 

653 return { 

654 "runconfigs": runconfigs, 

655 "statuses": statuses, 

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

657 "query_key": history.query.query_key, 

658 } 

659 

660 

661class IssueAddFilterSerializer(serializers.Serializer): 

662 filters = serializers.ListField( 

663 child=serializers.IntegerField(), 

664 allow_empty=False, 

665 write_only=True, 

666 ) 

667 

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

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

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

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

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

673 missing_ids: set[int] = requested_ids - found_ids 

674 

675 if missing_ids: 

676 raise serializers.ValidationError( 

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

678 ) 

679 internal["filters"] = filters 

680 return internal 

681 

682 

683class PassRateStatisticsSerializer(serializers.Serializer): 

684 passrate = RateSerializer() 

685 runrate = RateSerializer() 

686 discarded_rate = RateSerializer() 

687 notrun_rate = RateSerializer() 

688 

689 

690def serialize_MetricPassRatePerTest(metric_passrate): 

691 discarded_status = "discarded (expected)" 

692 

693 tests = OrderedDict() 

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

695 if results.is_fully_discarded: 

696 tests[str(test)] = { 

697 "status": discarded_status, 

698 "is_pass": False, 

699 "is_run": False, 

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

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

702 } 

703 else: 

704 tests[str(test)] = { 

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

706 "is_pass": results.is_pass, 

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

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

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

710 } 

711 

712 statuses = OrderedDict() 

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

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

715 

716 return { 

717 "tests": tests, 

718 "statuses": statuses, 

719 "discarded_status": discarded_status, 

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

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

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

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

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

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

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

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

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

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

730 "query_key": metric_passrate.query.query_key, 

731 }