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,403 @@
1
+ from uuid import UUID, uuid1, uuid4
2
+ from datetime import datetime
3
+ from enum import Enum, IntEnum, auto
4
+ from cassandra.cqlengine.models import Model
5
+ from cassandra.cqlengine.usertype import UserType
6
+ from cassandra.cqlengine import columns
7
+ from cassandra.util import uuid_from_time, unix_time_from_uuid1
8
+
9
+ from argus.backend.models.github_issue import GithubIssue, IssueLink
10
+ from argus.backend.models.plan import ArgusReleasePlan
11
+ from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData, ArgusGraphView
12
+ from argus.backend.models.runtime_store import RuntimeStore
13
+ from argus.backend.models.view_widgets import WidgetHighlights, WidgetComment
14
+ from argus.backend.models.argus_ai import ErrorEventEmbeddings, CriticalEventEmbeddings
15
+
16
+
17
+ def uuid_now():
18
+ return uuid_from_time(datetime.utcnow())
19
+
20
+
21
+ class ArgusTestException(Exception):
22
+ pass
23
+
24
+
25
+ class UserRoles(str, Enum):
26
+ User = "ROLE_USER"
27
+ Manager = "ROLE_MANAGER"
28
+ Admin = "ROLE_ADMIN"
29
+
30
+
31
+ class User(Model):
32
+ id = columns.UUID(primary_key=True, default=uuid4)
33
+ username = columns.Text(index=True)
34
+ full_name = columns.Text()
35
+ password = columns.Text()
36
+ email = columns.Text(index=True)
37
+ registration_date = columns.DateTime()
38
+ roles = columns.List(value_type=columns.Text)
39
+ picture_id = columns.UUID(default=None)
40
+ api_token = columns.Text(index=True)
41
+
42
+ def __hash__(self) -> int:
43
+ return hash(self.id)
44
+
45
+ def is_manager(self) -> bool:
46
+ return UserRoles.Manager in self.roles
47
+
48
+ def is_admin(self) -> bool:
49
+ return UserRoles.Admin in self.roles
50
+
51
+ def set_as_admin(self) -> None:
52
+ if UserRoles.Admin not in self.roles:
53
+ self.roles.append(UserRoles.Admin.value)
54
+
55
+ def set_as_manager(self) -> None:
56
+ if UserRoles.Manager not in self.roles:
57
+ self.roles.append(UserRoles.Manager.value)
58
+
59
+ def get_id(self):
60
+ return str(self.id)
61
+
62
+ @classmethod
63
+ def exists(cls, user_id: UUID):
64
+ try:
65
+ user = cls.get(id=user_id)
66
+ if user:
67
+ return user
68
+ except cls.DoesNotExist:
69
+ pass
70
+ return None
71
+
72
+ @classmethod
73
+ def exists_by_name(cls, name: str):
74
+ try:
75
+ user = cls.get(username=name)
76
+ if user:
77
+ return user
78
+ except cls.DoesNotExist:
79
+ pass
80
+ return None
81
+
82
+ def __str__(self):
83
+ return f"User('{self.id}','{self.username}')"
84
+
85
+ def to_json(self):
86
+ return {
87
+ "id": str(self.id),
88
+ "username": self.username,
89
+ "full_name": self.full_name,
90
+ "picture_id": self.picture_id
91
+ }
92
+
93
+
94
+ class Team(Model):
95
+ id = columns.UUID(primary_key=True, default=uuid4)
96
+ name = columns.Text(required=True)
97
+ leader = columns.UUID(index=True, required=True)
98
+ members = columns.List(value_type=columns.UUID)
99
+ motd = columns.Text()
100
+
101
+
102
+ class UserOauthToken(Model):
103
+ id = columns.UUID(primary_key=True, default=uuid4)
104
+ user_id = columns.UUID(index=True, required=True)
105
+ kind = columns.Text(required=True, index=True)
106
+ token = columns.Text(required=True)
107
+
108
+
109
+ class ArgusRelease(Model):
110
+ __table_name__ = "argus_release_v2"
111
+ id = columns.UUID(primary_key=True, default=uuid4)
112
+ name = columns.Text(index=True, required=True)
113
+ pretty_name = columns.Text()
114
+ description = columns.Text()
115
+ github_repo_url = columns.Text()
116
+ valid_version_regex = columns.Text()
117
+ assignee = columns.List(value_type=columns.UUID)
118
+ picture_id = columns.UUID()
119
+ enabled = columns.Boolean(default=lambda: True)
120
+ perpetual = columns.Boolean(default=lambda: False)
121
+ dormant = columns.Boolean(default=lambda: False)
122
+
123
+ def __eq__(self, other):
124
+ if isinstance(other, ArgusRelease):
125
+ return self.name == other.name
126
+ else:
127
+ return super().__eq__(other)
128
+
129
+
130
+ class ArgusGroup(Model):
131
+ __table_name__ = "argus_group_v2"
132
+ id = columns.UUID(primary_key=True, default=uuid4)
133
+ release_id = columns.UUID(required=True, index=True)
134
+ name = columns.Text(required=True, index=True)
135
+ pretty_name = columns.Text()
136
+ description = columns.Text()
137
+ assignee = columns.List(value_type=columns.UUID)
138
+ build_system_id = columns.Text()
139
+ enabled = columns.Boolean(default=lambda: True)
140
+
141
+ def __hash__(self) -> int:
142
+ return hash((self.id, self.release_id))
143
+
144
+ def __eq__(self, other):
145
+ if isinstance(other, ArgusGroup):
146
+ return self.name == other.name and self.release_id == other.release_id
147
+ else:
148
+ return super().__eq__(other)
149
+
150
+
151
+ class ArgusUserView(Model):
152
+ id = columns.UUID(primary_key=True, partition_key=True, default=uuid4)
153
+ name = columns.Text(required=True, index=True)
154
+ display_name = columns.Text()
155
+ description = columns.Text()
156
+ user_id = columns.UUID(required=True, index=True)
157
+ plan_id = columns.UUID(index=True)
158
+ tests = columns.List(value_type=columns.UUID, default=lambda: [])
159
+ release_ids = columns.List(value_type=columns.UUID, default=lambda: [])
160
+ group_ids = columns.List(value_type=columns.UUID, default=lambda: [])
161
+ created = columns.DateTime(default=datetime.utcnow)
162
+ last_updated = columns.DateTime(default=datetime.utcnow)
163
+ widget_settings = columns.Text(required=True)
164
+
165
+
166
+ class ArgusTest(Model):
167
+ __table_name__ = "argus_test_v2"
168
+ id = columns.UUID(primary_key=True, default=uuid4)
169
+ group_id = columns.UUID(required=True, index=True)
170
+ release_id = columns.UUID(required=True, index=True)
171
+ name = columns.Text(required=True, index=True)
172
+ pretty_name = columns.Text()
173
+ description = columns.Text()
174
+ assignee = columns.List(value_type=columns.UUID)
175
+ build_system_id = columns.Text(index=True)
176
+ enabled = columns.Boolean(default=lambda: True)
177
+ build_system_url = columns.Text()
178
+ plugin_name = columns.Text()
179
+ plugin_subtype = columns.Text()
180
+
181
+ def __eq__(self, other):
182
+ if isinstance(other, ArgusTest):
183
+ return self.name == other.name and self.group_id == other.group_id and self.release_id == other.release_id
184
+ else:
185
+ return super().__eq__(other)
186
+
187
+ def validate_build_system_id(self):
188
+ try:
189
+ t = ArgusTest.get(build_system_id=self.build_system_id)
190
+ if t.id != self.id:
191
+ raise ArgusTestException("Build Id is already used by another test", t.id, self.id)
192
+ except ArgusTest.DoesNotExist:
193
+ pass
194
+
195
+
196
+ class ArgusTestRunComment(Model):
197
+ id = columns.UUID(primary_key=True, default=uuid4, partition_key=True)
198
+ test_run_id = columns.UUID(required=True, index=True)
199
+ user_id = columns.UUID(required=True, index=True)
200
+ release_id = columns.UUID(required=True, index=True)
201
+ test_id = columns.UUID(required=True, index=True)
202
+ posted_at = columns.Integer(
203
+ required=True, clustering_order="desc", primary_key=True)
204
+ message = columns.Text(min_length=1, max_length=65535)
205
+ mentions = columns.List(value_type=columns.UUID, default=[])
206
+ reactions = columns.Map(key_type=columns.Text, value_type=columns.Integer)
207
+
208
+
209
+ class ArgusEventTypes(str, Enum):
210
+ AssigneeChanged = "ARGUS_ASSIGNEE_CHANGE"
211
+ TestRunStatusChanged = "ARGUS_TEST_RUN_STATUS_CHANGE"
212
+ TestRunInvestigationStatusChanged = "ARGUS_TEST_RUN_INVESTIGATION_STATUS_CHANGE"
213
+ TestRunBatchInvestigationStatusChange = "ARGUS_TEST_RUN_INVESTIGATION_BATCH_STATUS_CHANGE"
214
+ TestRunCommentPosted = "ARGUS_TEST_RUN_COMMENT_POSTED"
215
+ TestRunCommentUpdated = "ARGUS_TEST_RUN_COMMENT_UPDATED"
216
+ TestRunCommentDeleted = "ARGUS_TEST_RUN_COMMENT_DELETED"
217
+ TestRunIssueAdded = "ARGUS_TEST_RUN_ISSUE_ADDED"
218
+ TestRunIssueRemoved = "ARGUS_TEST_RUN_ISSUE_REMOVED"
219
+
220
+
221
+ class ArgusEvent(Model):
222
+ id = columns.UUID(primary_key=True, default=uuid4, partition_key=True)
223
+ release_id = columns.UUID(index=True)
224
+ group_id = columns.UUID(index=True)
225
+ test_id = columns.UUID(index=True)
226
+ run_id = columns.UUID(index=True)
227
+ user_id = columns.UUID(index=True)
228
+ kind = columns.Text(required=True, index=True)
229
+ body = columns.Text(required=True)
230
+ created_at = columns.DateTime(required=True)
231
+
232
+
233
+ class ArgusNotificationTypes(str, Enum):
234
+ Mention = "TYPE_MENTION"
235
+ StatusChange = "TYPE_STATUS_CHANGE"
236
+ AssigneeChange = "TYPE_ASSIGNEE_CHANGE"
237
+ ScheduleChange = "TYPE_SCHEDULE_CHANGE"
238
+ ViewActionItemAssignee = "TYPE_VIEW_ACTION_ITEM_ASSIGNEE"
239
+ ViewHighlightMention = "TYPE_VIEW_HIGHLIGHT_MENTION"
240
+
241
+
242
+ class ArgusNotificationSourceTypes(str, Enum):
243
+ TestRun = "TEST_RUN"
244
+ Schedule = "SCHEDULE"
245
+ Comment = "COMMENT"
246
+ ViewActionItem = "VIEW_ACTION_ITEM"
247
+ ViewHighlight = "VIEW_HIGHLIGHT"
248
+
249
+
250
+ class ArgusNotificationState(IntEnum):
251
+ UNREAD = auto()
252
+ READ = auto()
253
+
254
+
255
+ class ArgusNotification(Model):
256
+ receiver = columns.UUID(primary_key=True, partition_key=True)
257
+ id = columns.TimeUUID(primary_key=True, clustering_order="DESC", default=uuid_now)
258
+ type = columns.Text(required=True)
259
+ state = columns.SmallInt(required=True, default=lambda: ArgusNotificationState.UNREAD)
260
+ sender = columns.UUID(required=True)
261
+ source_type = columns.Text(required=True)
262
+ source_id = columns.UUID(required=True)
263
+ title = columns.Text(required=True, max_length=1024)
264
+ content = columns.Text(required=True, max_length=65535)
265
+
266
+ def to_dict_short_summary(self) -> dict:
267
+ return {
268
+ "receiver": self.receiver,
269
+ "sender": self.sender,
270
+ "id": self.id,
271
+ "created": unix_time_from_uuid1(self.id) * 1000,
272
+ "title": self.title,
273
+ "state": self.state,
274
+ }
275
+
276
+ def to_dict(self) -> dict:
277
+ return {
278
+ "receiver": self.receiver,
279
+ "sender": self.sender,
280
+ "id": self.id,
281
+ "created": unix_time_from_uuid1(self.id) * 1000,
282
+ "title": self.title,
283
+ "type": self.type,
284
+ "content": self.content,
285
+ "source": self.source_type,
286
+ "source_id": self.source_id,
287
+ "state": self.state,
288
+ }
289
+
290
+
291
+ class ArgusGithubIssue(Model):
292
+ # FIXME: Deprecated. To be removed.
293
+ id = columns.UUID(primary_key=True, default=uuid4, partition_key=True)
294
+ added_on = columns.DateTime(default=datetime.utcnow)
295
+ release_id = columns.UUID(index=True)
296
+ group_id = columns.UUID(index=True)
297
+ test_id = columns.UUID(index=True)
298
+ run_id = columns.UUID(index=True)
299
+ user_id = columns.UUID(index=True)
300
+ type = columns.Text()
301
+ owner = columns.Text()
302
+ repo = columns.Text()
303
+ issue_number = columns.Integer()
304
+ last_status = columns.Text()
305
+ title = columns.Text()
306
+ url = columns.Text()
307
+
308
+ def __hash__(self) -> int:
309
+ return hash((self.owner, self.repo, self.issue_number))
310
+
311
+ def __eq__(self, other):
312
+ if isinstance(other, ArgusGithubIssue):
313
+ return self.owner == other.owner and self.repo == other.repo and self.issue_number == other.issue_number
314
+ return super().__eq__(other)
315
+
316
+ def __ne__(self, other):
317
+ return not self == other
318
+
319
+
320
+ class ArgusSchedule(Model):
321
+ __table_name__ = "argus_schedule_v4"
322
+ release_id = columns.UUID(primary_key=True, required=True)
323
+ id = columns.TimeUUID(primary_key=True, default=uuid1, clustering_order="DESC")
324
+ period_start = columns.DateTime(required=True, default=datetime.utcnow)
325
+ period_end = columns.DateTime(required=True, primary_key=True, clustering_order="DESC")
326
+ tag = columns.Text(default="")
327
+
328
+
329
+ class ArgusScheduleAssignee(Model):
330
+ __table_name__ = "argus_schedule_user_v3"
331
+ assignee = columns.UUID(primary_key=True)
332
+ id = columns.TimeUUID(primary_key=True, default=uuid1,
333
+ clustering_order="DESC")
334
+ schedule_id = columns.TimeUUID(required=True, index=True)
335
+ release_id = columns.UUID(required=True)
336
+
337
+
338
+ class ArgusScheduleTest(Model):
339
+ __table_name__ = "argus_schedule_test_v5"
340
+ test_id = columns.UUID(primary_key=True, required=True)
341
+ id = columns.TimeUUID(primary_key=True, default=uuid1,
342
+ clustering_order="DESC")
343
+ schedule_id = columns.TimeUUID(required=True, index=True)
344
+ release_id = columns.UUID(partition_key=True)
345
+
346
+
347
+ class ArgusScheduleGroup(Model):
348
+ __table_name__ = "argus_schedule_group_v3"
349
+ group_id = columns.UUID(partition_key=True, required=True)
350
+ id = columns.TimeUUID(primary_key=True, default=uuid1,
351
+ clustering_order="DESC")
352
+ schedule_id = columns.TimeUUID(required=True, index=True)
353
+ release_id = columns.UUID(partition_key=True)
354
+
355
+
356
+ class ReleasePlannerComment(Model):
357
+ __table_name__ = "argus_planner_comment_v2"
358
+ release = columns.UUID(primary_key=True)
359
+ group = columns.UUID(primary_key=True)
360
+ test = columns.UUID(primary_key=True)
361
+ comment = columns.Text(default=lambda: "")
362
+
363
+
364
+ class WebFileStorage(Model):
365
+ id = columns.UUID(primary_key=True, default=uuid4)
366
+ filepath = columns.Text(min_length=1)
367
+ filename = columns.Text(min_length=1)
368
+
369
+
370
+ USED_MODELS: list[Model] = [
371
+ RuntimeStore,
372
+ User,
373
+ UserOauthToken,
374
+ Team,
375
+ WebFileStorage,
376
+ ArgusRelease,
377
+ ArgusUserView,
378
+ ArgusGroup,
379
+ ArgusTest,
380
+ ArgusTestRunComment,
381
+ ArgusEvent,
382
+ ReleasePlannerComment,
383
+ ArgusNotification,
384
+ ArgusSchedule,
385
+ ArgusScheduleAssignee,
386
+ ArgusScheduleGroup,
387
+ ArgusScheduleTest,
388
+ ArgusGenericResultMetadata,
389
+ ArgusGenericResultData,
390
+ ArgusBestResultData,
391
+ ArgusReleasePlan,
392
+ WidgetHighlights,
393
+ WidgetComment,
394
+ ArgusGraphView,
395
+ ErrorEventEmbeddings,
396
+ CriticalEventEmbeddings,
397
+ GithubIssue,
398
+ IssueLink,
399
+ ]
400
+
401
+ USED_TYPES: list[UserType] = [
402
+
403
+ ]
File without changes
@@ -0,0 +1,248 @@
1
+ import logging
2
+ from datetime import datetime
3
+ from math import ceil
4
+ from uuid import UUID
5
+ from time import time
6
+
7
+ from cassandra.cqlengine import columns
8
+ from cassandra.cqlengine.models import Model
9
+ from cassandra.cqlengine.usertype import UserType
10
+ from flask import Blueprint
11
+ from argus.backend.db import ScyllaCluster
12
+ from argus.backend.models.plan import ArgusReleasePlan
13
+ from argus.backend.models.web import (
14
+ ArgusTest,
15
+ ArgusGroup,
16
+ ArgusRelease,
17
+ ArgusScheduleGroup,
18
+ ArgusSchedule,
19
+ ArgusScheduleTest,
20
+ ArgusScheduleAssignee
21
+ )
22
+ from argus.backend.util.common import chunk
23
+ from argus.common.enums import TestInvestigationStatus, TestStatus
24
+
25
+ LOGGER = logging.getLogger(__name__)
26
+
27
+
28
+ class PluginModelBase(Model):
29
+ _plugin_name = "unknown"
30
+ # Metadata
31
+ build_id = columns.Text(required=True, partition_key=True)
32
+ start_time = columns.DateTime(required=True, primary_key=True, clustering_order="DESC",
33
+ default=datetime.utcnow, custom_index=True)
34
+ id = columns.UUID(index=True, required=True)
35
+ release_id = columns.UUID(index=True)
36
+ group_id = columns.UUID(index=True)
37
+ test_id = columns.UUID(index=True)
38
+ assignee = columns.UUID(index=True)
39
+ status = columns.Text(default=lambda: TestStatus.CREATED.value)
40
+ investigation_status = columns.Text(default=lambda: TestInvestigationStatus.NOT_INVESTIGATED.value)
41
+ heartbeat = columns.Integer(default=lambda: int(time()))
42
+ end_time = columns.DateTime(default=lambda: datetime.utcfromtimestamp(0))
43
+ build_job_url = columns.Text()
44
+ product_version = columns.Text(index=True)
45
+
46
+ # Test Logs Collection
47
+ logs = columns.List(value_type=columns.Tuple(columns.Text(), columns.Text()))
48
+
49
+ @classmethod
50
+ def table_name(cls) -> str:
51
+ return cls.__table_name__
52
+
53
+ @classmethod
54
+ def _stats_query(cls) -> str:
55
+ raise NotImplementedError()
56
+
57
+ def assign_categories(self):
58
+ key = self.build_id
59
+ try:
60
+ test: ArgusTest = ArgusTest.get(build_system_id=key)
61
+ self.release_id = test.release_id
62
+ self.group_id = test.group_id
63
+ self.test_id = test.id
64
+ if not test.plugin_name or test.plugin_name != self._plugin_name:
65
+ test.plugin_name = self._plugin_name
66
+ test.save()
67
+ except ArgusTest.DoesNotExist:
68
+ LOGGER.warning("Test entity missing for key \"%s\", run won't be visible until this is corrected", key)
69
+
70
+ def get_assignment(self, version: str | None = None) -> UUID | None:
71
+ associated_test: ArgusTest = ArgusTest.get(build_system_id=self.build_id)
72
+ associated_release: ArgusRelease = ArgusRelease.get(id=associated_test.release_id)
73
+
74
+ plans: list[ArgusReleasePlan] = list(ArgusReleasePlan.filter(release_id=associated_release.id))
75
+
76
+ if version:
77
+ plans = [plan for plan in plans if plan.target_version == version]
78
+
79
+ for plan in plans:
80
+ if associated_test.group_id in plan.groups:
81
+ return plan.assignee_mapping.get(associated_test.group_id, plan.owner)
82
+ if associated_test.id in plan.tests:
83
+ return plan.assignee_mapping.get(associated_test.id, plan.owner)
84
+
85
+ # FIXME: Legacy fallback until we fully migrate to new plans
86
+ return self._legacy_get_scheduled_assignee(associated_test=associated_test, associated_release=associated_release)
87
+
88
+ def get_scheduled_assignee(self) -> UUID:
89
+ return self.get_assignment()
90
+
91
+ def _legacy_get_scheduled_assignee(self, associated_test: ArgusTest, associated_release: ArgusRelease) -> UUID:
92
+ """
93
+ Iterate over all schedules (groups and tests) and return first available assignee
94
+ """
95
+ associated_group = ArgusGroup.get(id=associated_test.group_id)
96
+
97
+ scheduled_groups = ArgusScheduleGroup.filter(
98
+ release_id=associated_release.id,
99
+ group_id=associated_group.id,
100
+ ).all()
101
+
102
+ scheduled_tests = ArgusScheduleTest.filter(
103
+ release_id=associated_release.id,
104
+ test_id=associated_test.id
105
+ ).all()
106
+
107
+ unique_schedule_ids = {scheduled_obj.schedule_id for scheduled_obj in [
108
+ *scheduled_tests, *scheduled_groups]}
109
+
110
+ schedules = ArgusSchedule.filter(
111
+ release_id=associated_release.id,
112
+ id__in=tuple(unique_schedule_ids),
113
+ ).all()
114
+
115
+ today = datetime.utcnow()
116
+
117
+ valid_schedules = []
118
+ for schedule in schedules:
119
+ if schedule.period_start <= today <= schedule.period_end:
120
+ valid_schedules.append(schedule)
121
+
122
+ assignees_uuids = []
123
+ for schedule in valid_schedules:
124
+ assignees = ArgusScheduleAssignee.filter(
125
+ schedule_id=schedule.id
126
+ ).all()
127
+ assignees_uuids.extend([assignee.assignee for assignee in assignees])
128
+
129
+ return assignees_uuids[0] if len(assignees_uuids) > 0 else None
130
+
131
+ @classmethod
132
+ def get_jobs_assigned_to_user(cls, user_id: str | UUID):
133
+ cluster = ScyllaCluster.get()
134
+ query = cluster.prepare("SELECT build_id, start_time, release_id, group_id, assignee, "
135
+ f"test_id, id, status, investigation_status, build_job_url, scylla_version FROM {cls.table_name()} WHERE assignee = ?")
136
+ rows = cluster.session.execute(query=query, parameters=(user_id,))
137
+
138
+ return list(rows)
139
+
140
+ @classmethod
141
+ def get_jobs_meta_by_test_id(cls, test_id: UUID):
142
+ cluster = ScyllaCluster.get()
143
+ query = cluster.prepare(
144
+ f"SELECT build_id, start_time, id, test_id, release_id, group_id, status, investigation_status FROM {cls.table_name()} WHERE test_id = ?")
145
+ rows = cluster.session.execute(query=query, parameters=(test_id,))
146
+
147
+ return list(rows)
148
+
149
+ @classmethod
150
+ def prepare_investigation_status_update_query(cls, build_id: str, start_time: datetime, new_status: TestInvestigationStatus):
151
+ cluster = ScyllaCluster.get()
152
+ query = cluster.prepare(
153
+ f"UPDATE {cls.table_name()} SET investigation_status = ? WHERE build_id = ? AND start_time = ?")
154
+ bound_query = query.bind(values=(new_status.value, build_id, start_time))
155
+
156
+ return bound_query
157
+
158
+ @classmethod
159
+ def get_stats_for_release(cls, release: ArgusRelease, build_ids=list[str]):
160
+ cluster = ScyllaCluster.get()
161
+ query = cluster.prepare(cls._stats_query())
162
+ futures = []
163
+ step_size = 90
164
+
165
+ for step in range(0, ceil(len(build_ids) / step_size)):
166
+ start_pos = step*step_size
167
+ next_slice = build_ids[start_pos:start_pos+step_size]
168
+ futures.append(cluster.session.execute_async(query=query, parameters=(next_slice,)))
169
+
170
+ return futures
171
+
172
+ @classmethod
173
+ def get_run_meta_by_build_id(cls, build_id: str, limit: int = 10):
174
+ cluster = ScyllaCluster.get()
175
+ query = cluster.prepare("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
176
+ f"assignee, end_time, investigation_status, heartbeat FROM {cls.table_name()} WHERE build_id = ? LIMIT ?")
177
+ rows = cluster.session.execute(query=query, parameters=(build_id, limit))
178
+
179
+ return list(rows)
180
+
181
+ @classmethod
182
+ def get_run_meta_by_run_id(cls, run_id: UUID | str):
183
+ cluster = ScyllaCluster.get()
184
+ query = cluster.prepare("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
185
+ f"assignee, end_time, investigation_status, heartbeat FROM {cls.table_name()} WHERE id = ?")
186
+ rows = cluster.session.execute(query=query, parameters=(run_id,))
187
+
188
+ return list(rows)
189
+
190
+ @classmethod
191
+ def load_test_run(cls, run_id: UUID) -> 'PluginModelBase':
192
+ raise NotImplementedError()
193
+
194
+ @classmethod
195
+ def submit_run(cls, request_data: dict) -> 'PluginModelBase':
196
+ raise NotImplementedError()
197
+
198
+ @classmethod
199
+ def get_distinct_product_versions(cls, release: ArgusRelease) -> list[str]:
200
+ raise NotImplementedError()
201
+
202
+ @classmethod
203
+ def get_distinct_versions_for_view(cls, tests: list[ArgusTest]) -> list[str]:
204
+ cluster = ScyllaCluster.get()
205
+ statement = cluster.prepare(f"SELECT scylla_version FROM {cls.table_name()} WHERE build_id IN ?")
206
+ futures = []
207
+ for batch in chunk(tests):
208
+ futures.append(cluster.session.execute_async(query=statement,
209
+ parameters=([t.build_system_id for t in batch],)))
210
+
211
+ rows = []
212
+ for future in futures:
213
+ rows.extend(future.result())
214
+ unique_versions = {r["scylla_version"] for r in rows if r["scylla_version"]}
215
+
216
+ return sorted(list(unique_versions), reverse=True)
217
+
218
+ def update_heartbeat(self):
219
+ self.heartbeat = int(time())
220
+
221
+ def change_status(self, new_status: TestStatus):
222
+ self.status = new_status.value
223
+
224
+ def change_investigation_status(self, new_investigation_status: TestInvestigationStatus):
225
+ self.investigation_status = new_investigation_status.value
226
+
227
+ def submit_product_version(self, version: str):
228
+ raise NotImplementedError()
229
+
230
+ def set_full_version(self, version: str):
231
+ self.product_version = version
232
+
233
+ def submit_logs(self, logs: list[dict]):
234
+ raise NotImplementedError()
235
+
236
+ def finish_run(self, payload: dict = None):
237
+ raise NotImplementedError()
238
+
239
+ def sut_timestamp(self, sut_package_name) -> float:
240
+ raise NotImplementedError()
241
+
242
+
243
+ class PluginInfoBase:
244
+ name: str
245
+ controller: Blueprint
246
+ model: PluginModelBase
247
+ all_models: list[Model]
248
+ all_types: list[UserType]