argus-alm 0.14.2__py3-none-any.whl → 0.15.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. argus/_version.py +21 -0
  2. argus/backend/.gitkeep +0 -0
  3. argus/backend/__init__.py +0 -0
  4. argus/backend/cli.py +57 -0
  5. argus/backend/controller/__init__.py +0 -0
  6. argus/backend/controller/admin.py +20 -0
  7. argus/backend/controller/admin_api.py +355 -0
  8. argus/backend/controller/api.py +589 -0
  9. argus/backend/controller/auth.py +67 -0
  10. argus/backend/controller/client_api.py +109 -0
  11. argus/backend/controller/main.py +316 -0
  12. argus/backend/controller/notification_api.py +72 -0
  13. argus/backend/controller/notifications.py +13 -0
  14. argus/backend/controller/planner_api.py +194 -0
  15. argus/backend/controller/team.py +129 -0
  16. argus/backend/controller/team_ui.py +19 -0
  17. argus/backend/controller/testrun_api.py +513 -0
  18. argus/backend/controller/view_api.py +188 -0
  19. argus/backend/controller/views_widgets/__init__.py +0 -0
  20. argus/backend/controller/views_widgets/graphed_stats.py +54 -0
  21. argus/backend/controller/views_widgets/graphs.py +68 -0
  22. argus/backend/controller/views_widgets/highlights.py +135 -0
  23. argus/backend/controller/views_widgets/nemesis_stats.py +26 -0
  24. argus/backend/controller/views_widgets/summary.py +43 -0
  25. argus/backend/db.py +98 -0
  26. argus/backend/error_handlers.py +41 -0
  27. argus/backend/events/event_processors.py +34 -0
  28. argus/backend/models/__init__.py +0 -0
  29. argus/backend/models/argus_ai.py +24 -0
  30. argus/backend/models/github_issue.py +60 -0
  31. argus/backend/models/plan.py +24 -0
  32. argus/backend/models/result.py +187 -0
  33. argus/backend/models/runtime_store.py +58 -0
  34. argus/backend/models/view_widgets.py +25 -0
  35. argus/backend/models/web.py +403 -0
  36. argus/backend/plugins/__init__.py +0 -0
  37. argus/backend/plugins/core.py +248 -0
  38. argus/backend/plugins/driver_matrix_tests/controller.py +66 -0
  39. argus/backend/plugins/driver_matrix_tests/model.py +429 -0
  40. argus/backend/plugins/driver_matrix_tests/plugin.py +21 -0
  41. argus/backend/plugins/driver_matrix_tests/raw_types.py +62 -0
  42. argus/backend/plugins/driver_matrix_tests/service.py +61 -0
  43. argus/backend/plugins/driver_matrix_tests/udt.py +42 -0
  44. argus/backend/plugins/generic/model.py +86 -0
  45. argus/backend/plugins/generic/plugin.py +15 -0
  46. argus/backend/plugins/generic/types.py +14 -0
  47. argus/backend/plugins/loader.py +39 -0
  48. argus/backend/plugins/sct/controller.py +224 -0
  49. argus/backend/plugins/sct/plugin.py +37 -0
  50. argus/backend/plugins/sct/resource_setup.py +177 -0
  51. argus/backend/plugins/sct/service.py +682 -0
  52. argus/backend/plugins/sct/testrun.py +288 -0
  53. argus/backend/plugins/sct/udt.py +100 -0
  54. argus/backend/plugins/sirenada/model.py +118 -0
  55. argus/backend/plugins/sirenada/plugin.py +16 -0
  56. argus/backend/service/admin.py +26 -0
  57. argus/backend/service/argus_service.py +696 -0
  58. argus/backend/service/build_system_monitor.py +185 -0
  59. argus/backend/service/client_service.py +127 -0
  60. argus/backend/service/event_service.py +18 -0
  61. argus/backend/service/github_service.py +233 -0
  62. argus/backend/service/jenkins_service.py +269 -0
  63. argus/backend/service/notification_manager.py +159 -0
  64. argus/backend/service/planner_service.py +608 -0
  65. argus/backend/service/release_manager.py +229 -0
  66. argus/backend/service/results_service.py +690 -0
  67. argus/backend/service/stats.py +610 -0
  68. argus/backend/service/team_manager_service.py +82 -0
  69. argus/backend/service/test_lookup.py +172 -0
  70. argus/backend/service/testrun.py +489 -0
  71. argus/backend/service/user.py +308 -0
  72. argus/backend/service/views.py +219 -0
  73. argus/backend/service/views_widgets/__init__.py +0 -0
  74. argus/backend/service/views_widgets/graphed_stats.py +180 -0
  75. argus/backend/service/views_widgets/highlights.py +374 -0
  76. argus/backend/service/views_widgets/nemesis_stats.py +34 -0
  77. argus/backend/template_filters.py +27 -0
  78. argus/backend/tests/__init__.py +0 -0
  79. argus/backend/tests/client_service/__init__.py +0 -0
  80. argus/backend/tests/client_service/test_submit_results.py +79 -0
  81. argus/backend/tests/conftest.py +180 -0
  82. argus/backend/tests/results_service/__init__.py +0 -0
  83. argus/backend/tests/results_service/test_best_results.py +178 -0
  84. argus/backend/tests/results_service/test_cell.py +65 -0
  85. argus/backend/tests/results_service/test_chartjs_additional_functions.py +259 -0
  86. argus/backend/tests/results_service/test_create_chartjs.py +220 -0
  87. argus/backend/tests/results_service/test_result_metadata.py +100 -0
  88. argus/backend/tests/results_service/test_results_service.py +203 -0
  89. argus/backend/tests/results_service/test_validation_rules.py +213 -0
  90. argus/backend/tests/view_widgets/__init__.py +0 -0
  91. argus/backend/tests/view_widgets/test_highlights_api.py +532 -0
  92. argus/backend/util/common.py +65 -0
  93. argus/backend/util/config.py +38 -0
  94. argus/backend/util/encoders.py +56 -0
  95. argus/backend/util/logsetup.py +80 -0
  96. argus/backend/util/module_loaders.py +30 -0
  97. argus/backend/util/send_email.py +91 -0
  98. argus/client/base.py +1 -3
  99. argus/client/driver_matrix_tests/cli.py +17 -8
  100. argus/client/generic/cli.py +4 -2
  101. argus/client/generic/client.py +1 -0
  102. argus/client/generic_result.py +48 -9
  103. argus/client/sct/client.py +1 -3
  104. argus/client/sirenada/client.py +4 -1
  105. argus/client/tests/__init__.py +0 -0
  106. argus/client/tests/conftest.py +19 -0
  107. argus/client/tests/test_package.py +45 -0
  108. argus/client/tests/test_results.py +224 -0
  109. argus/common/sct_types.py +3 -0
  110. argus/common/sirenada_types.py +1 -1
  111. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/METADATA +43 -19
  112. argus_alm-0.15.2.dist-info/RECORD +122 -0
  113. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/WHEEL +2 -1
  114. argus_alm-0.15.2.dist-info/entry_points.txt +3 -0
  115. argus_alm-0.15.2.dist-info/top_level.txt +1 -0
  116. argus_alm-0.14.2.dist-info/RECORD +0 -20
  117. argus_alm-0.14.2.dist-info/entry_points.txt +0 -4
  118. {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,608 @@
1
+
2
+ import logging
3
+ import datetime
4
+ import json
5
+ from collections import defaultdict
6
+ from copy import deepcopy
7
+ from dataclasses import dataclass
8
+ from functools import reduce
9
+ from typing import Any, Optional, TypedDict
10
+ from uuid import UUID
11
+ from flask import g
12
+ from slugify import slugify
13
+
14
+ from argus.backend.models.plan import ArgusReleasePlan
15
+ from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, ArgusUserView, User
16
+ from argus.backend.service.jenkins_service import JenkinsService
17
+ from argus.backend.service.test_lookup import TestLookup
18
+ from argus.backend.service.views import UserViewService
19
+ from argus.backend.util.common import chunk
20
+
21
+
22
+ LOGGER = logging.getLogger(__name__)
23
+
24
+
25
+ @dataclass(frozen=True, init=True, repr=True, kw_only=True)
26
+ class CreatePlanPayload:
27
+ name: str
28
+ description: str
29
+ owner: str
30
+ participants: list[str]
31
+ target_version: str | None
32
+ release_id: str
33
+ tests: list[str]
34
+ groups: list[str]
35
+ assignments: dict[str, str]
36
+ view_id: Optional[str] = None
37
+ created_from: Optional[str] = None
38
+
39
+
40
+ @dataclass(frozen=True, init=True, repr=True, kw_only=True)
41
+ class TempPlanPayload:
42
+ id: str
43
+ name: str
44
+ completed: bool
45
+ description: str
46
+ owner: str
47
+ participants: list[str]
48
+ target_version: str
49
+ assignee_mapping: dict[str, str]
50
+ assignments: dict[str, str] = None
51
+ release_id: str
52
+ tests: list[str]
53
+ groups: list[str]
54
+ creation_time: str
55
+ last_updated: str
56
+ ends_at: str
57
+ created_from: Optional[str]
58
+ view_id: Optional[str] = None
59
+
60
+
61
+ @dataclass(frozen=True, init=True, repr=True, kw_only=True)
62
+ class CopyPlanPayload:
63
+ plan: TempPlanPayload
64
+ keepParticipants: bool
65
+ replacements: dict[str, str]
66
+ targetReleaseId: str
67
+ targetReleaseName: str
68
+
69
+
70
+ class PlanTriggerPayload(TypedDict):
71
+ plan_id: str | None
72
+ release: str | None
73
+ version: str | None
74
+ common_params: dict[str, str]
75
+ params: list[dict[str, str]]
76
+
77
+
78
+ class PlannerServiceException(Exception):
79
+ pass
80
+
81
+
82
+ class PlanningService:
83
+
84
+ VIEW_WIDGET_SETTINGS = [
85
+ {
86
+ "position": 1,
87
+ "type": "githubIssues",
88
+ "filter": [],
89
+ "settings": {
90
+ "submitDisabled": True,
91
+ "aggregateByIssue": True
92
+ }
93
+ },
94
+ {
95
+ "position": 2,
96
+ "type": "releaseStats",
97
+ "filter": [],
98
+ "settings": {
99
+ "horizontal": False,
100
+ "displayExtendedStats": True,
101
+ "hiddenStatuses": ["not_run", "not_planned"]
102
+ }
103
+ },
104
+ {
105
+ "position": 3,
106
+ "type": "testDashboard",
107
+ "filter": [],
108
+ "settings": {
109
+ "targetVersion": True,
110
+ "versionsIncludeNoVersion": False,
111
+ "productVersion": None
112
+ }
113
+ }
114
+ ]
115
+
116
+ def version(self):
117
+ return "v1"
118
+
119
+ def create_plan(self, payload: dict[str, Any]) -> ArgusReleasePlan:
120
+ plan_request = CreatePlanPayload(**payload)
121
+
122
+ try:
123
+ existing = ArgusReleasePlan.filter(
124
+ name=plan_request.name, target_version=plan_request.target_version).allow_filtering().get()
125
+ if existing:
126
+ raise PlannerServiceException(
127
+ f"Found existing plan {existing.name} ({existing.target_version}) with the same name and version", existing, plan_request)
128
+ except ArgusReleasePlan.DoesNotExist:
129
+ pass
130
+
131
+ plan = ArgusReleasePlan()
132
+ plan.name = plan_request.name
133
+ plan.description = plan_request.description
134
+ plan.owner = UUID(plan_request.owner)
135
+ plan.target_version = plan_request.target_version
136
+ plan.release_id = UUID(plan_request.release_id)
137
+ plan.participants = plan_request.participants
138
+ plan.assignee_mapping = {UUID(entity_id): UUID(user_id)
139
+ for entity_id, user_id in plan_request.assignments.items()}
140
+ plan.groups = plan_request.groups
141
+ plan.tests = plan_request.tests
142
+ if plan_request.created_from:
143
+ plan.created_from = plan_request.created_from
144
+ if not plan_request.view_id:
145
+ view = self.create_view_for_plan(plan)
146
+ plan.view_id = view.id
147
+ else:
148
+ plan.view_id = plan_request.view_id
149
+ view = self.update_view_for_plan(plan, existing=True)
150
+
151
+ plan.save()
152
+
153
+ return plan
154
+
155
+ def update_plan(self, payload: dict[str, Any]) -> bool:
156
+ plan_request = TempPlanPayload(**payload)
157
+
158
+ try:
159
+ existing = ArgusReleasePlan.filter(
160
+ name=plan_request.name, target_version=plan_request.target_version).allow_filtering().get()
161
+ if existing and existing.id != UUID(plan_request.id):
162
+ raise PlannerServiceException(
163
+ f"Found existing plan {existing.name} ({existing.target_version}) with the same name and version", existing, plan_request)
164
+ except ArgusReleasePlan.DoesNotExist:
165
+ pass
166
+
167
+ plan: ArgusReleasePlan = ArgusReleasePlan.get(id=plan_request.id)
168
+ plan.owner = plan_request.owner
169
+ plan.participants = plan_request.participants
170
+ plan.assignee_mapping = plan_request.assignee_mapping
171
+ plan.tests = plan_request.tests
172
+ plan.groups = plan_request.groups
173
+ plan.name = plan_request.name
174
+ plan.target_version = plan_request.target_version
175
+ plan.description = plan_request.description
176
+ plan.last_updated = datetime.datetime.now(tz=datetime.UTC)
177
+
178
+ if plan_request.view_id:
179
+ if plan_request.view_id != plan.view_id:
180
+ try:
181
+ old_view: ArgusUserView = ArgusUserView.get(id=plan.view_id)
182
+ old_view.plan_id = None
183
+ old_view.save()
184
+ except ArgusUserView.DoesNotExist:
185
+ pass
186
+ plan.view_id = plan_request.view_id
187
+ view = self.update_view_for_plan(plan, existing=True)
188
+ else:
189
+ if plan.view_id:
190
+ view: ArgusUserView = ArgusUserView.get(id=plan.view_id)
191
+ view.plan_id = None
192
+ view.save()
193
+ view = self.create_view_for_plan(plan)
194
+ plan.view_id = view.id
195
+
196
+ plan.save()
197
+
198
+ return True
199
+
200
+ def update_view_for_plan(self, plan: ArgusReleasePlan, existing: bool = False) -> ArgusUserView:
201
+ service = UserViewService()
202
+ release: ArgusRelease = ArgusRelease.get(id=plan.release_id)
203
+
204
+ version_str = f" ({plan.target_version}) " if plan.target_version else ""
205
+ view_name = f"{release.name} {version_str}- {plan.name}"
206
+
207
+ view: ArgusUserView = ArgusUserView.get(id=plan.view_id)
208
+ if view.plan_id and view.plan_id != plan.id:
209
+ raise PlannerServiceException("This view is already assigned to another plan.")
210
+ view.plan_id = plan.id
211
+ settings = json.loads(view.widget_settings)
212
+ items = [f"test:{tid}" for tid in plan.tests]
213
+ items = [*items, *[f"group:{gid}" for gid in plan.groups]]
214
+ entities = service.parse_view_entity_list(items)
215
+ view.tests = entities["tests"]
216
+ if not existing:
217
+ view.display_name = view_name
218
+ view.name = slugify(view_name)
219
+ view.description = f"{plan.target_version or ''} Automatic view for the release plan \"{plan.name}\". {plan.description}"
220
+ view.group_ids = entities["group"]
221
+
222
+ dash = next(filter(lambda widget: widget["type"] == "testDashboard", settings), None)
223
+ if dash:
224
+ dash["settings"]["productVersion"] = plan.target_version
225
+ dash["settings"]["targetVersion"] = bool(plan.target_version)
226
+
227
+ view.widget_settings = json.dumps(settings)
228
+ view.save()
229
+ service.refresh_stale_view(view)
230
+ return view
231
+
232
+ def create_view_for_plan(self, plan: ArgusReleasePlan) -> ArgusUserView:
233
+ service = UserViewService()
234
+ release: ArgusRelease = ArgusRelease.get(id=plan.release_id)
235
+ items = [f"test:{tid}" for tid in plan.tests]
236
+ items = [*items, *[f"group:{gid}" for gid in plan.groups]]
237
+ version_str = f" ({plan.target_version}) " if plan.target_version else ""
238
+ view_name = f"{release.name} {version_str}- {plan.name}"
239
+ settings = deepcopy(self.VIEW_WIDGET_SETTINGS)
240
+ if plan.target_version:
241
+ settings[2]["settings"]["productVersion"] = plan.target_version
242
+ else:
243
+ settings[2]["settings"]["targetVersion"] = False
244
+ view = service.create_view(
245
+ name=slugify(view_name),
246
+ display_name=view_name,
247
+ description=f"{plan.target_version or ''} Automatic view for the release plan \"{plan.name}\". {plan.description}",
248
+ items=items,
249
+ plan_id=plan.id,
250
+ widget_settings=json.dumps(settings),
251
+ )
252
+
253
+ view.save()
254
+ service.refresh_stale_view(view)
255
+ return view
256
+
257
+ def change_plan_owner(self, plan_id: UUID | str, new_owner: UUID | str) -> bool:
258
+ user: User = User.get(id=new_owner)
259
+ plan: ArgusReleasePlan = ArgusReleasePlan.get(id=plan_id)
260
+
261
+ plan.owner = user.id
262
+ plan.last_updated = datetime.datetime.now(tz=datetime.UTC)
263
+
264
+ plan.save()
265
+ return True
266
+
267
+ def get_plan(self, plan_id: str | UUID) -> ArgusReleasePlan:
268
+ return ArgusReleasePlan.get(id=plan_id)
269
+
270
+ def get_gridview_for_release(self, release_id: str | UUID) -> dict[str, dict]:
271
+ release = ArgusRelease.get(id=release_id)
272
+ release = TestLookup.index_mapper(release, "release")
273
+ groups: list[ArgusGroup] = list(ArgusGroup.filter(release_id=release_id).all())
274
+ tests: list[ArgusTest] = list(ArgusTest.filter(release_id=release_id).all())
275
+
276
+ groups = {str(g.id): TestLookup.index_mapper(g, "group") for g in groups if g.enabled}
277
+
278
+ tests_by_group = reduce(lambda acc, test: acc[str(test.group_id)].append(test) or acc, tests, defaultdict(list))
279
+
280
+ res = {
281
+ "tests": {str(t.id): TestLookup.index_mapper(t) for t in tests if t.enabled and groups.get(str(t.group_id), {}).get("enabled", False)},
282
+ "groups": groups,
283
+ "testByGroup": tests_by_group
284
+ }
285
+
286
+ for group in res["groups"].values():
287
+ group["release"] = release["name"]
288
+
289
+ for test in res["tests"].values():
290
+ g = res["groups"][str(test["group_id"])]
291
+ test["group"] = g["pretty_name"] or g["name"]
292
+ test["release"] = release["name"]
293
+
294
+ return res
295
+
296
+ def copy_plan(self, payload: CopyPlanPayload) -> ArgusReleasePlan:
297
+
298
+ try:
299
+ existing = ArgusReleasePlan.filter(
300
+ name=payload.plan.name, target_version=payload.plan.target_version).allow_filtering().get()
301
+ if existing:
302
+ raise PlannerServiceException(
303
+ f"Found existing plan {existing.name} ({existing.target_version}) with the same name and version", existing, payload)
304
+ except ArgusReleasePlan.DoesNotExist:
305
+ pass
306
+
307
+ original_plan: ArgusReleasePlan = ArgusReleasePlan.get(id=payload.plan.id)
308
+ target_release: ArgusRelease = ArgusRelease.get(id=payload.targetReleaseId)
309
+ original_release: ArgusRelease = ArgusRelease.get(id=original_plan.release_id)
310
+
311
+ original_tests: list[ArgusTest] = ArgusTest.filter(id__in=original_plan.tests).all()
312
+ original_groups: list[ArgusGroup] = ArgusGroup.filter(id__in=original_plan.groups).all()
313
+ target_tests: list[ArgusTest] = ArgusTest.filter(release_id=target_release.id).all()
314
+ target_groups: list[ArgusGroup] = ArgusGroup.filter(release_id=target_release.id).all()
315
+
316
+ tests_by_build_id = {t.build_system_id: t for t in target_tests}
317
+ groups_by_build_id = {g.build_system_id: g for g in target_groups}
318
+
319
+ new_tests = []
320
+ new_groups = []
321
+ new_assignee_mapping = {}
322
+
323
+ for test in original_tests:
324
+ original_assignee = original_plan.assignee_mapping.get(test.id)
325
+ new_build_id = test.build_system_id.replace(original_release.name, target_release.name, 1)
326
+ new_test = tests_by_build_id.get(new_build_id)
327
+ new_test_id = new_test.id if new_test else payload.replacements.get(test.id)
328
+ if new_test_id:
329
+ new_tests.append(new_test_id)
330
+ if original_assignee and payload.keepParticipants:
331
+ new_assignee_mapping[new_test_id] = original_assignee
332
+
333
+ for group in original_groups:
334
+ original_assignee = original_plan.assignee_mapping.get(group.id)
335
+ new_build_id = group.build_system_id.replace(original_release.name, target_release.name, 1)
336
+ new_group = groups_by_build_id.get(new_build_id)
337
+ new_group_id = new_group.id if new_group else payload.replacements.get(group.id)
338
+ if new_group_id:
339
+ new_groups.append(new_group_id)
340
+ if original_assignee and payload.keepParticipants:
341
+ new_assignee_mapping[new_group_id] = original_assignee
342
+
343
+ new_plan = ArgusReleasePlan()
344
+ new_plan.release_id = target_release.id
345
+ new_plan.owner = payload.plan.owner
346
+ new_plan.name = payload.plan.name
347
+ new_plan.description = payload.plan.description
348
+ if payload.keepParticipants:
349
+ new_plan.participants = payload.plan.participants
350
+ new_plan.assignee_mapping = new_assignee_mapping
351
+ new_plan.tests = new_tests
352
+ new_plan.groups = new_groups
353
+ new_plan.target_version = payload.plan.target_version
354
+ view = self.create_view_for_plan(new_plan)
355
+ new_plan.view_id = view.id
356
+
357
+ new_plan.save()
358
+
359
+ return new_plan
360
+
361
+ def check_plan_copy_eligibility(self, plan_id: str | UUID, target_release_id: str | UUID) -> dict:
362
+ target_release: ArgusRelease = ArgusRelease.get(id=target_release_id)
363
+ plan: ArgusReleasePlan = ArgusReleasePlan.get(id=plan_id)
364
+ original_release: ArgusRelease = ArgusRelease.get(id=plan.release_id)
365
+
366
+ original_tests: list[ArgusTest] = ArgusTest.filter(id__in=plan.tests).all()
367
+ original_groups: list[ArgusGroup] = ArgusGroup.filter(id__in=plan.groups).all()
368
+
369
+ target_tests: list[ArgusTest] = ArgusTest.filter(release_id=target_release.id).all()
370
+ target_groups: list[ArgusGroup] = ArgusGroup.filter(release_id=target_release.id).all()
371
+
372
+ tests_by_build_id = {t.build_system_id: t for t in target_tests}
373
+ groups_by_build_id = {g.build_system_id: g for g in target_groups}
374
+
375
+ missing_tests = []
376
+ missing_groups = []
377
+ status = "passed"
378
+ for test in original_tests:
379
+ new_build_id = test.build_system_id.replace(original_release.name, target_release.name, 1)
380
+ new_group = tests_by_build_id.get(new_build_id)
381
+ if not new_group:
382
+ t = TestLookup.index_mapper(test)
383
+ t["release"] = original_release.name
384
+ missing_tests.append(t)
385
+
386
+ for group in original_groups:
387
+ new_build_id = group.build_system_id.replace(original_release.name, target_release.name, 1)
388
+ new_group = groups_by_build_id.get(new_build_id)
389
+ if not new_group:
390
+ g = TestLookup.index_mapper(group)
391
+ g["release"] = original_release.name
392
+ missing_groups.append(g)
393
+
394
+ if len(missing_tests) > 0 or len(missing_groups) > 0:
395
+ status = "failed"
396
+
397
+ return {
398
+ "status": status,
399
+ "targetRelease": target_release,
400
+ "originalRelease": original_release,
401
+ "missing": {
402
+ "tests": missing_tests,
403
+ "groups": missing_groups,
404
+ }
405
+ }
406
+
407
+ def release_planner(self, release_name: str) -> dict[str, Any]:
408
+ release: ArgusRelease = ArgusRelease.get(name=release_name)
409
+
410
+ plans: list[ArgusReleasePlan] = self.get_plans_for_release(release.id)
411
+
412
+ return {
413
+ "release": release,
414
+ "plans": plans,
415
+ }
416
+
417
+ def get_plans_for_release(self, release_id: str | UUID) -> list[ArgusReleasePlan]:
418
+ return list(ArgusReleasePlan.filter(release_id=release_id).all())
419
+
420
+ def delete_plan(self, plan_id: str | UUID, delete_view: bool = True):
421
+ plan: ArgusReleasePlan = ArgusReleasePlan.get(id=plan_id)
422
+ if plan.view_id:
423
+ view: ArgusUserView = ArgusUserView.get(id=plan.view_id)
424
+ if delete_view:
425
+ view.delete()
426
+ else:
427
+ view.plan_id = None
428
+ view.save()
429
+
430
+ plan.delete()
431
+ return True
432
+
433
+ def get_assignee_for_test(self, test_id: str | UUID, target_version: str = None) -> UUID | None:
434
+ dml = ArgusReleasePlan.filter(tests__contains=test_id, complete=False)
435
+ if target_version:
436
+ dml.filter(target_version=target_version)
437
+ potential_plans: list[ArgusReleasePlan] = dml.allow_filtering().all()
438
+ for plan in potential_plans:
439
+ # Use the most recent plan
440
+ return plan.assignee_mapping.get(test_id, plan.owner)
441
+ return None
442
+
443
+ def get_assignee_for_group(self, group_id: str | UUID, target_version: str = None) -> UUID | None:
444
+ dml = ArgusReleasePlan.filter(groups__contains=group_id, complete=False)
445
+ if target_version:
446
+ dml.filter(target_version=target_version)
447
+ potential_plans: list[ArgusReleasePlan] = dml.allow_filtering().all()
448
+ for plan in potential_plans:
449
+ # Use the most recent plan
450
+ return plan.assignee_mapping.get(group_id, plan.owner)
451
+ return None
452
+
453
+ def get_assignments_for_groups(self, release_id: str | UUID, version: str = None, plan_id: UUID = None) -> dict[str, UUID]:
454
+ release: ArgusRelease = ArgusRelease.get(id=release_id)
455
+ if not plan_id:
456
+ plans: list[ArgusReleasePlan] = list(ArgusReleasePlan.filter(release_id=release.id).all())
457
+ plans = plans if not version else [plan for plan in plans if plan.target_version == version]
458
+ else:
459
+ plans = [ArgusReleasePlan.get(id=plan_id)]
460
+
461
+ all_assignments = {}
462
+ for plan in reversed(plans):
463
+ # TODO: (gid, [user_id]) Should be changed to gid, user_id once old scheduling mechanism is completely removed
464
+ all_assignments.update(map(lambda group_id: (
465
+ str(group_id), [plan.assignee_mapping.get(group_id, plan.owner)]), plan.groups))
466
+
467
+ return all_assignments
468
+
469
+ def get_assignments_for_tests(self, group_id: str | UUID, version: str = None, plan_id: UUID | str = None) -> dict[str, UUID]:
470
+ group: ArgusGroup = ArgusGroup.get(id=group_id)
471
+ release: ArgusRelease = ArgusRelease.get(id=group.release_id)
472
+ if not plan_id:
473
+ plans: list[ArgusReleasePlan] = list(ArgusReleasePlan.filter(release_id=release.id).all())
474
+ plans = plans if not version else [plan for plan in plans if plan.target_version == version]
475
+ else:
476
+ plans = [ArgusReleasePlan.get(id=plan_id)]
477
+
478
+ all_assignments = {}
479
+
480
+ def get_assignee(test_id: UUID, mapping: dict[UUID, UUID]):
481
+ test_assignment = mapping.get(test_id)
482
+ return test_assignment
483
+
484
+ for plan in reversed(plans):
485
+ # TODO: (tid, [user_id]) Should be changed to tid, user_id once old scheduling mechanism is completely removed
486
+ all_assignments.update(map(lambda test_id: (str(test_id), [get_assignee(
487
+ test_id, plan.assignee_mapping) or plan.owner]), plan.tests))
488
+
489
+ return all_assignments
490
+
491
+ def complete_plan(self, plan_id: str | UUID) -> bool:
492
+ plan: ArgusReleasePlan = ArgusReleasePlan(id=plan_id).get()
493
+ plan.completed = True
494
+
495
+ plan.save()
496
+ return plan.completed
497
+
498
+ def resolve_plan(self, plan_id: str | UUID) -> list[dict[str, Any]]:
499
+ plan: ArgusReleasePlan = ArgusReleasePlan.get(id=plan_id)
500
+
501
+ release: ArgusRelease = ArgusRelease.get(id=plan.release_id)
502
+ tests: list[ArgusTest] = []
503
+ for batch in chunk(plan.tests):
504
+ tests.extend(ArgusTest.filter(id__in=batch).all())
505
+ test_groups: list[ArgusGroup] = ArgusGroup.filter(id__in=list({t.group_id for t in tests})).all()
506
+ test_groups = {g.id: g for g in test_groups}
507
+ groups: list[ArgusGroup] = list(ArgusGroup.filter(id__in=plan.groups).all())
508
+
509
+ mapped = [TestLookup.index_mapper(entity, "group" if isinstance(
510
+ entity, ArgusGroup) else "test") for entity in [*tests, *groups]]
511
+
512
+ for ent in mapped:
513
+ ent["release"] = release.name
514
+ if group_id := ent.get("group_id"):
515
+ group = test_groups.get(group_id)
516
+ ent["group"] = group.pretty_name or group.name
517
+
518
+ return mapped
519
+
520
+ def trigger_jobs(self, payload: PlanTriggerPayload) -> bool:
521
+
522
+ release_name = payload.get("release")
523
+ plan_id = payload.get("plan_id")
524
+ version = payload.get("version")
525
+
526
+ condition_set = (bool(release_name), bool(plan_id), bool(version))
527
+
528
+ match condition_set:
529
+ case (True, False, False):
530
+ release = ArgusRelease.get(name=release_name)
531
+ filter_expr = {"release_id__eq": release.id}
532
+ case (False, True, False):
533
+ filter_expr = {"id__eq": plan_id}
534
+ case (False, False, True):
535
+ filter_expr = {"target_version__eq": version}
536
+ case (True, False, True):
537
+ release = ArgusRelease.get(name=release_name)
538
+ filter_expr = {"target_version__eq": version, "release_id__eq": release.id}
539
+ case _:
540
+ raise PlannerServiceException("No version, release name or plan id specified.", payload)
541
+
542
+ plans: list[ArgusReleasePlan] = list(ArgusReleasePlan.filter(**filter_expr).allow_filtering().all())
543
+
544
+ if len(plans) == 0:
545
+ return False, "No plans to trigger"
546
+
547
+ common_params = payload.get("common_params", {})
548
+ params = payload.get("params", [])
549
+ test_ids = [test_id for plan in plans for test_id in plan.tests]
550
+ group_ids = [group_id for plan in plans for group_id in plan.groups]
551
+
552
+ tests = []
553
+ for batch in chunk(test_ids):
554
+ tests.extend(ArgusTest.filter(id__in=batch).all())
555
+
556
+ for batch in (chunk(group_ids)):
557
+ tests.extend(ArgusTest.filter(group_id__in=batch).allow_filtering().all())
558
+
559
+ tests = list({test for test in tests})
560
+
561
+ LOGGER.info("Will trigger %s tests...", len(tests))
562
+
563
+ service = JenkinsService()
564
+ failures = []
565
+ successes = []
566
+ for test in tests:
567
+ try:
568
+ latest_build_number = service.latest_build(test.build_system_id)
569
+ if latest_build_number == -1:
570
+ failures.append(test.build_system_id)
571
+ continue
572
+ raw_params = service.retrieve_job_parameters(test.build_system_id, latest_build_number)
573
+ job_params = {param["name"]: param["value"] for param in raw_params if param.get("value")}
574
+ backend = job_params.get("backend")
575
+ match backend.split("-"):
576
+ case ["aws", *_]:
577
+ region_key = "region"
578
+ case ["gce", *_]:
579
+ region_key = "gce_datacenter"
580
+ case ["azure", *_]:
581
+ region_key = "azure_region_name"
582
+ case _:
583
+ raise PlannerServiceException(f"Unknown backend encountered: {backend}", backend)
584
+
585
+ job_params = None
586
+ for param_set in params:
587
+ if param_set["test"] == "longevity" and backend == param_set["backend"]:
588
+ job_params = dict(param_set)
589
+ job_params.pop("type", None)
590
+ region = job_params.pop("region", None)
591
+ job_params[region_key] = region
592
+ break
593
+ if not job_params:
594
+ raise PlannerServiceException(
595
+ f"Parameters not found for job {test.build_system_id}", test.build_system_id)
596
+ final_params = {**job_params, **common_params, **job_params}
597
+ queue_item = service.build_job(test.build_system_id, final_params, g.user.username)
598
+ info = service.get_queue_info(queue_item)
599
+ url = info.get("url", info.get("taskUrl", ""))
600
+ successes.append(url)
601
+ except Exception:
602
+ LOGGER.error("Failed to trigger %s", test.build_system_id, exc_info=True)
603
+ failures.append(test.build_system_id)
604
+
605
+ return {
606
+ "jobs": successes,
607
+ "failed_to_execute": failures,
608
+ }