argus-alm 0.12.3__py3-none-any.whl → 0.12.4b2__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 (31) hide show
  1. argus/backend/controller/admin_api.py +26 -0
  2. argus/backend/controller/api.py +26 -1
  3. argus/backend/controller/main.py +21 -0
  4. argus/backend/controller/testrun_api.py +16 -0
  5. argus/backend/controller/view_api.py +162 -0
  6. argus/backend/models/web.py +16 -0
  7. argus/backend/plugins/core.py +25 -10
  8. argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
  9. argus/backend/plugins/driver_matrix_tests/model.py +251 -3
  10. argus/backend/plugins/driver_matrix_tests/raw_types.py +27 -0
  11. argus/backend/plugins/driver_matrix_tests/service.py +18 -0
  12. argus/backend/plugins/driver_matrix_tests/udt.py +14 -13
  13. argus/backend/plugins/generic/model.py +5 -2
  14. argus/backend/plugins/sct/service.py +13 -1
  15. argus/backend/service/argus_service.py +116 -20
  16. argus/backend/service/build_system_monitor.py +37 -7
  17. argus/backend/service/jenkins_service.py +2 -1
  18. argus/backend/service/release_manager.py +14 -0
  19. argus/backend/service/stats.py +147 -11
  20. argus/backend/service/testrun.py +44 -5
  21. argus/backend/service/views.py +258 -0
  22. argus/backend/template_filters.py +7 -0
  23. argus/backend/util/common.py +14 -2
  24. argus/client/driver_matrix_tests/cli.py +110 -0
  25. argus/client/driver_matrix_tests/client.py +56 -193
  26. argus_alm-0.12.4b2.dist-info/METADATA +129 -0
  27. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/RECORD +30 -27
  28. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/entry_points.txt +1 -0
  29. argus_alm-0.12.3.dist-info/METADATA +0 -207
  30. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/LICENSE +0 -0
  31. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/WHEEL +0 -0
@@ -0,0 +1,258 @@
1
+ import datetime
2
+ import logging
3
+ import re
4
+ from functools import partial, reduce
5
+ from typing import Any, Callable, TypedDict
6
+ from urllib.parse import unquote
7
+ from uuid import UUID
8
+
9
+ from cassandra.cqlengine.models import Model
10
+ from argus.backend.models.web import ArgusGroup, ArgusRelease, ArgusTest, ArgusUserView, User
11
+ from argus.backend.plugins.loader import all_plugin_models
12
+ from argus.backend.util.common import chunk, current_user
13
+
14
+ LOGGER = logging.getLogger(__name__)
15
+
16
+
17
+ class UserViewException(Exception):
18
+ pass
19
+
20
+
21
+ class ViewUpdateRequest(TypedDict):
22
+ name: str
23
+ description: str
24
+ display_name: str
25
+ tests: list[str]
26
+ widget_settings: str
27
+
28
+
29
+ class UserViewService:
30
+ ADD_ALL_ID = UUID("db6f33b2-660b-4639-ba7f-79725ef96616")
31
+ def create_view(self, name: str, items: list[str], widget_settings: str, description: str = None, display_name: str = None) -> ArgusUserView:
32
+ try:
33
+ name_check = ArgusUserView.get(name=name)
34
+ raise UserViewException(f"View with name {name} already exists: {name_check.id}", name, name_check, name_check.id)
35
+ except ArgusUserView.DoesNotExist:
36
+ pass
37
+ view = ArgusUserView()
38
+ view.name = name
39
+ view.display_name = display_name or name
40
+ view.description = description
41
+ view.widget_settings = widget_settings
42
+ view.tests = []
43
+ for entity in items:
44
+ entity_type, entity_id = entity.split(":")
45
+ match (entity_type):
46
+ case "release":
47
+ view.tests.extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
48
+ view.release_ids.append(entity_id)
49
+ case "group":
50
+ view.tests.extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
51
+ view.group_ids.append(entity_id)
52
+ case "test":
53
+ view.tests.append(entity_id)
54
+ view.user_id = current_user().id
55
+
56
+ view.save()
57
+ return view
58
+
59
+ @staticmethod
60
+ def index_mapper(item: Model, type = "test"):
61
+ mapped = dict(item)
62
+ mapped["type"] = type
63
+ return mapped
64
+
65
+ def test_lookup(self, query: str):
66
+ def check_visibility(entity: dict):
67
+ if not entity["enabled"]:
68
+ return False
69
+ if entity.get("group") and not entity["group"]["enabled"]:
70
+ return False
71
+ if entity.get("release") and not entity["release"]["enabled"]:
72
+ return False
73
+ return True
74
+
75
+ def facet_extraction(query: str) -> str:
76
+ extractor = re.compile(r"(?:(?P<name>(?:release|group|type)):(?P<value>\"?[\w\d\.\-]*\"?))")
77
+ facets = re.findall(extractor, query)
78
+
79
+ return (re.sub(extractor, "", query).strip(), facets)
80
+
81
+ def type_facet_filter(item: dict, key: str, facet_query: str):
82
+ entity_type: str = item[key]
83
+ return facet_query.lower() == entity_type
84
+
85
+ def facet_filter(item: dict, key: str, facet_query: str):
86
+ if entity := item.get(key):
87
+ name: str = entity.get("pretty_name") or entity.get("name")
88
+ return facet_query.lower() in name.lower() if name else False
89
+ return False
90
+
91
+ def facet_wrapper(query_func: Callable[[dict], bool], facet_query: str, facet_type: str) -> bool:
92
+ def inner(item: dict, query: str):
93
+ return query_func(item, query) and facet_funcs[facet_type](item, facet_type, facet_query)
94
+ return inner
95
+
96
+ facet_funcs = {
97
+ "type": type_facet_filter,
98
+ "release": facet_filter,
99
+ "group": facet_filter,
100
+ }
101
+
102
+ def index_searcher(item, query: str):
103
+ name: str = item["pretty_name"] or item["name"]
104
+ return unquote(query).lower() in name.lower() if query else True
105
+
106
+ text_query, facets = facet_extraction(query)
107
+ search_func = index_searcher
108
+ for facet, value in facets:
109
+ if facet in facet_funcs.keys():
110
+ search_func = facet_wrapper(query_func=search_func, facet_query=value, facet_type=facet)
111
+
112
+
113
+ all_tests = ArgusTest.all()
114
+ all_releases = ArgusRelease.all()
115
+ all_groups = ArgusGroup.all()
116
+ release_by_id = {release.id: partial(self.index_mapper, type="release")(release) for release in all_releases}
117
+ group_by_id = {group.id: partial(self.index_mapper, type="group")(group) for group in all_groups}
118
+ index = [self.index_mapper(t) for t in all_tests]
119
+ index = [*release_by_id.values(), *group_by_id.values(), *index]
120
+ for item in index:
121
+ item["group"] = group_by_id.get(item.get("group_id"))
122
+ item["release"] = release_by_id.get(item.get("release_id"))
123
+
124
+ results = filter(partial(search_func, query=text_query), index)
125
+
126
+ return [{ "id": self.ADD_ALL_ID, "name": "Add all...", "type": "special" }, *list(res for res in results if check_visibility(res))]
127
+
128
+ def update_view(self, view_id: str | UUID, update_data: ViewUpdateRequest) -> bool:
129
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
130
+ if view.user_id != current_user().id and not current_user().is_admin():
131
+ raise UserViewException("Unable to modify other users' views")
132
+ for key in ["user_id", "id"]:
133
+ update_data.pop(key, None)
134
+ items = update_data.pop("items")
135
+ for k, value in update_data.items():
136
+ view[k] = value
137
+ view.tests = []
138
+ view.release_ids = []
139
+ view.group_ids = []
140
+ for entity in items:
141
+ entity_type, entity_id = entity.split(":")
142
+ match (entity_type):
143
+ case "release":
144
+ view.tests.extend(t.id for t in ArgusTest.filter(release_id=entity_id).all())
145
+ view.release_ids.append(entity_id)
146
+ case "group":
147
+ view.tests.extend(t.id for t in ArgusTest.filter(group_id=entity_id).all())
148
+ view.group_ids.append(entity_id)
149
+ case "test":
150
+ view.tests.append(entity_id)
151
+ view.last_updated = datetime.datetime.utcnow()
152
+ view.save()
153
+ return True
154
+
155
+ def delete_view(self, view_id: str | UUID) -> bool:
156
+ view = ArgusUserView.get(id=view_id)
157
+ if view.user_id != current_user().id and not current_user().is_admin():
158
+ raise UserViewException("Unable to modify other users' views")
159
+ view.delete()
160
+
161
+ return True
162
+
163
+ def get_view(self, view_id: str | UUID) -> ArgusUserView:
164
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
165
+ if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
166
+ self.refresh_stale_view(view)
167
+ return view
168
+
169
+ def get_view_by_name(self, view_name: str) -> ArgusUserView:
170
+ view: ArgusUserView = ArgusUserView.get(name=view_name)
171
+ if datetime.datetime.utcnow() - (view.last_updated or datetime.datetime.fromtimestamp(0)) > datetime.timedelta(hours=1):
172
+ self.refresh_stale_view(view)
173
+ return view
174
+
175
+ def get_all_views(self, user: User | None = None) -> list[ArgusUserView]:
176
+ if user:
177
+ return list(ArgusUserView.filter(user_id=user.id).all())
178
+ return list(ArgusUserView.filter().all())
179
+
180
+ def resolve_view_tests(self, view_id: str | UUID) -> list[ArgusTest]:
181
+ view = ArgusUserView.get(id=view_id)
182
+ return self.resolve_tests_by_id(view.tests)
183
+
184
+ def resolve_tests_by_id(self, test_ids: list[str | UUID]) -> list[ArgusTest]:
185
+ tests = []
186
+ for batch in chunk(test_ids):
187
+ tests.extend(ArgusTest.filter(id__in=batch).all())
188
+
189
+ return tests
190
+
191
+ def batch_resolve_entity(self, entity: Model, param_name: str, entity_ids: list[UUID]) -> list[Model]:
192
+ result = []
193
+ for batch in chunk(entity_ids):
194
+ result.extend(entity.filter(**{f"{param_name}__in": batch}).allow_filtering().all())
195
+ return result
196
+
197
+ def refresh_stale_view(self, view: ArgusUserView):
198
+ view.tests = [test.id for test in self.resolve_view_tests(view.id)]
199
+ all_tests = set(view.tests)
200
+ all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "group_id", view.group_ids))
201
+ all_tests.update(test.id for test in self.batch_resolve_entity(ArgusTest, "release_id", view.release_ids))
202
+ view.tests = list(all_tests)
203
+ view.last_updated = datetime.datetime.utcnow()
204
+ view.save()
205
+
206
+ return view
207
+
208
+ def resolve_releases_for_tests(self, tests: list[ArgusTest]):
209
+ releases = []
210
+ unique_release_ids = reduce(lambda releases, test: releases.add(test.release_id) or releases, tests, set())
211
+ for batch in chunk(unique_release_ids):
212
+ releases.extend(ArgusRelease.filter(id__in=batch).all())
213
+
214
+ return releases
215
+
216
+ def resolve_groups_for_tests(self, tests: list[ArgusTest]):
217
+ releases = []
218
+ unique_release_ids = reduce(lambda groups, test: groups.add(test.group_id) or groups, tests, set())
219
+ for batch in chunk(unique_release_ids):
220
+ releases.extend(ArgusGroup.filter(id__in=batch).all())
221
+
222
+ return releases
223
+
224
+ def get_versions_for_view(self, view_id: str | UUID) -> list[str]:
225
+ tests = self.resolve_view_tests(view_id)
226
+ unique_versions = {ver for plugin in all_plugin_models()
227
+ for ver in plugin.get_distinct_versions_for_view(tests=tests)}
228
+
229
+ return sorted(list(unique_versions), reverse=True)
230
+
231
+ def resolve_view_for_edit(self, view_id: str | UUID) -> dict:
232
+ view: ArgusUserView = ArgusUserView.get(id=view_id)
233
+ resolved = dict(view)
234
+ view_groups = self.batch_resolve_entity(ArgusGroup, "id", view.group_ids)
235
+ view_releases = self.batch_resolve_entity(ArgusRelease, "id", view.release_ids)
236
+ view_tests = self.resolve_view_tests(view.id)
237
+ all_groups = { group.id: partial(self.index_mapper, type="group")(group) for group in self.resolve_releases_for_tests(view_tests) }
238
+ all_releases ={ release.id: partial(self.index_mapper, type="release")(release) for release in self.resolve_releases_for_tests(view_tests) }
239
+ entities_by_id = {
240
+ entity.id: partial(self.index_mapper, type="release" if isinstance(entity, ArgusRelease) else "group")(entity)
241
+ for container in [view_releases, view_groups]
242
+ for entity in container
243
+ }
244
+
245
+ items = []
246
+ for test in view_tests:
247
+ if not (entities_by_id.get(test.group_id) or entities_by_id.get(test.release_id)):
248
+ item = dict(test)
249
+ item["type"] = "test"
250
+ items.append(item)
251
+
252
+ items = [*entities_by_id.values(), *items]
253
+ for entity in items:
254
+ entity["group"] = all_groups.get(entity.get("group_id"), {}).get("pretty_name") or all_groups.get(entity.get("group_id"), {}).get("name")
255
+ entity["release"] = all_releases.get(entity.get("release_id"), {}).get("pretty_name") or all_releases.get(entity.get("release_id"), {}).get("name")
256
+
257
+ resolved["items"] = items
258
+ return resolved
@@ -18,3 +18,10 @@ def safe_user(user: User):
18
18
  user_dict = dict(user.items())
19
19
  del user_dict["password"]
20
20
  return user_dict
21
+
22
+
23
+ @is_filter("formatted_date")
24
+ def formatted_date(date: datetime | None):
25
+ if date:
26
+ return date.strftime("%d/%m/%Y %H:%M:%S")
27
+ return "#unknown"
@@ -1,8 +1,11 @@
1
+ from itertools import islice
1
2
  import logging
2
- from typing import Callable
3
+ from typing import Callable, Iterable
3
4
  from uuid import UUID
4
5
 
5
- from flask import Request, Response
6
+ from flask import Request, Response, g
7
+
8
+ from argus.backend.models.web import User
6
9
 
7
10
 
8
11
  LOGGER = logging.getLogger(__name__)
@@ -20,6 +23,11 @@ def first(iterable, value, key: Callable = None, predicate: Callable = None):
20
23
  return None
21
24
 
22
25
 
26
+ def chunk(iterable: Iterable, slice_size = 90):
27
+ it = iter(iterable)
28
+ return iter(lambda: list(islice(it, slice_size)), [])
29
+
30
+
23
31
  def check_scheduled_test(test, group, testname):
24
32
  return testname in (f"{group}/{test}", test)
25
33
 
@@ -43,6 +51,10 @@ def get_payload(client_request: Request) -> dict:
43
51
  return request_payload
44
52
 
45
53
 
54
+ def current_user() -> User:
55
+ return g.user
56
+
57
+
46
58
  def get_build_number(build_job_url: str) -> int | None:
47
59
  build_number = build_job_url.rstrip("/").split("/")[-1] if build_job_url else -1
48
60
  if build_number:
@@ -0,0 +1,110 @@
1
+ import base64
2
+ import json
3
+ from pathlib import Path
4
+ import click
5
+ import logging
6
+ from argus.backend.util.enums import TestStatus
7
+
8
+ from argus.client.driver_matrix_tests.client import ArgusDriverMatrixClient
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ @click.group
14
+ def cli():
15
+ pass
16
+
17
+
18
+ def _submit_driver_result_internal(api_key: str, base_url: str, run_id: str, metadata_path: str):
19
+ metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
20
+ LOGGER.info("Submitting results for %s [%s/%s] to Argus...", run_id, metadata["driver_name"], metadata["driver_type"])
21
+ raw_xml = (Path(metadata_path).parent / metadata["junit_result"]).read_bytes()
22
+ client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url)
23
+ client.submit_driver_result(driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], raw_junit_data=base64.encodebytes(raw_xml))
24
+ LOGGER.info("Done.")
25
+
26
+ def _submit_driver_failure_internal(api_key: str, base_url: str, run_id: str, metadata_path: str):
27
+ metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
28
+ LOGGER.info("Submitting failure for %s [%s/%s] to Argus...", run_id, metadata["driver_name"], metadata["driver_type"])
29
+ client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url)
30
+ client.submit_driver_failure(driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], failure_reason=metadata["failure_reason"])
31
+ LOGGER.info("Done.")
32
+
33
+
34
+ @click.command("submit-run")
35
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
36
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
37
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
38
+ @click.option("--build-id", required=True, help="Unique job identifier in the build system, e.g. scylla-master/group/job for jenkins (The full path)")
39
+ @click.option("--build-url", required=True, help="Job URL in the build system")
40
+ def submit_driver_matrix_run(api_key: str, base_url: str, run_id: str, build_id: str, build_url: str):
41
+ LOGGER.info("Submitting %s (%s) to Argus...", build_id, run_id)
42
+ client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url)
43
+ client.submit_driver_matrix_run(job_name=build_id, job_url=build_url)
44
+ LOGGER.info("Done.")
45
+
46
+
47
+ @click.command("submit-driver")
48
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
49
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
50
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
51
+ @click.option("--metadata-path", required=True, help="Path to the metadata .json file that contains path to junit xml and other required information")
52
+ def submit_driver_result(api_key: str, base_url: str, run_id: str, metadata_path: str):
53
+ _submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id, metadata_path=metadata_path)
54
+
55
+
56
+ @click.command("fail-driver")
57
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
58
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
59
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
60
+ @click.option("--metadata-path", required=True, help="Path to the metadata .json file that contains path to junit xml and other required information")
61
+ def submit_driver_failure(api_key: str, base_url: str, run_id: str, metadata_path: str):
62
+ _submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id, metadata_path=metadata_path)
63
+
64
+
65
+ @click.command("submit-or-fail-driver")
66
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
67
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
68
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
69
+ @click.option("--metadata-path", required=True, help="Path to the metadata .json file that contains path to junit xml and other required information")
70
+ def submit_or_fail_driver(api_key: str, base_url: str, run_id: str, metadata_path: str):
71
+ metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
72
+ if metadata.get("failure_reason"):
73
+ _submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id, metadata_path=metadata_path)
74
+ else:
75
+ _submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id, metadata_path=metadata_path)
76
+
77
+
78
+ @click.command("submit-env")
79
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
80
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
81
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
82
+ @click.option("--env-path", required=True, help="Path to the Build-00.txt file that contains environment information about Scylla")
83
+ def submit_driver_env(api_key: str, base_url: str, run_id: str, env_path: str):
84
+ LOGGER.info("Submitting environment for run %s to Argus...", run_id)
85
+ raw_env = Path(env_path).read_text()
86
+ client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url)
87
+ client.submit_env(raw_env)
88
+ LOGGER.info("Done.")
89
+
90
+
91
+ @click.command("finish-run")
92
+ @click.option("--api-key", help="Argus API key for authorization", required=True)
93
+ @click.option("--base-url", default="https://argus.scylladb.com", help="Base URL for argus instance")
94
+ @click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
95
+ @click.option("--status", required=True, help="Resulting job status")
96
+ def finish_driver_matrix_run(api_key: str, base_url: str, run_id: str, status: str):
97
+ client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url)
98
+ client.finalize_run(run_type=ArgusDriverMatrixClient.test_type, run_id=run_id, body={"status": TestStatus(status)})
99
+
100
+
101
+ cli.add_command(submit_driver_matrix_run)
102
+ cli.add_command(submit_driver_result)
103
+ cli.add_command(submit_or_fail_driver)
104
+ cli.add_command(submit_driver_failure)
105
+ cli.add_command(submit_driver_env)
106
+ cli.add_command(finish_driver_matrix_run)
107
+
108
+
109
+ if __name__ == "__main__":
110
+ cli()