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.
- argus/_version.py +21 -0
- argus/backend/.gitkeep +0 -0
- argus/backend/__init__.py +0 -0
- argus/backend/cli.py +57 -0
- argus/backend/controller/__init__.py +0 -0
- argus/backend/controller/admin.py +20 -0
- argus/backend/controller/admin_api.py +355 -0
- argus/backend/controller/api.py +589 -0
- argus/backend/controller/auth.py +67 -0
- argus/backend/controller/client_api.py +109 -0
- argus/backend/controller/main.py +316 -0
- argus/backend/controller/notification_api.py +72 -0
- argus/backend/controller/notifications.py +13 -0
- argus/backend/controller/planner_api.py +194 -0
- argus/backend/controller/team.py +129 -0
- argus/backend/controller/team_ui.py +19 -0
- argus/backend/controller/testrun_api.py +513 -0
- argus/backend/controller/view_api.py +188 -0
- argus/backend/controller/views_widgets/__init__.py +0 -0
- argus/backend/controller/views_widgets/graphed_stats.py +54 -0
- argus/backend/controller/views_widgets/graphs.py +68 -0
- argus/backend/controller/views_widgets/highlights.py +135 -0
- argus/backend/controller/views_widgets/nemesis_stats.py +26 -0
- argus/backend/controller/views_widgets/summary.py +43 -0
- argus/backend/db.py +98 -0
- argus/backend/error_handlers.py +41 -0
- argus/backend/events/event_processors.py +34 -0
- argus/backend/models/__init__.py +0 -0
- argus/backend/models/argus_ai.py +24 -0
- argus/backend/models/github_issue.py +60 -0
- argus/backend/models/plan.py +24 -0
- argus/backend/models/result.py +187 -0
- argus/backend/models/runtime_store.py +58 -0
- argus/backend/models/view_widgets.py +25 -0
- argus/backend/models/web.py +403 -0
- argus/backend/plugins/__init__.py +0 -0
- argus/backend/plugins/core.py +248 -0
- argus/backend/plugins/driver_matrix_tests/controller.py +66 -0
- argus/backend/plugins/driver_matrix_tests/model.py +429 -0
- argus/backend/plugins/driver_matrix_tests/plugin.py +21 -0
- argus/backend/plugins/driver_matrix_tests/raw_types.py +62 -0
- argus/backend/plugins/driver_matrix_tests/service.py +61 -0
- argus/backend/plugins/driver_matrix_tests/udt.py +42 -0
- argus/backend/plugins/generic/model.py +86 -0
- argus/backend/plugins/generic/plugin.py +15 -0
- argus/backend/plugins/generic/types.py +14 -0
- argus/backend/plugins/loader.py +39 -0
- argus/backend/plugins/sct/controller.py +224 -0
- argus/backend/plugins/sct/plugin.py +37 -0
- argus/backend/plugins/sct/resource_setup.py +177 -0
- argus/backend/plugins/sct/service.py +682 -0
- argus/backend/plugins/sct/testrun.py +288 -0
- argus/backend/plugins/sct/udt.py +100 -0
- argus/backend/plugins/sirenada/model.py +118 -0
- argus/backend/plugins/sirenada/plugin.py +16 -0
- argus/backend/service/admin.py +26 -0
- argus/backend/service/argus_service.py +696 -0
- argus/backend/service/build_system_monitor.py +185 -0
- argus/backend/service/client_service.py +127 -0
- argus/backend/service/event_service.py +18 -0
- argus/backend/service/github_service.py +233 -0
- argus/backend/service/jenkins_service.py +269 -0
- argus/backend/service/notification_manager.py +159 -0
- argus/backend/service/planner_service.py +608 -0
- argus/backend/service/release_manager.py +229 -0
- argus/backend/service/results_service.py +690 -0
- argus/backend/service/stats.py +610 -0
- argus/backend/service/team_manager_service.py +82 -0
- argus/backend/service/test_lookup.py +172 -0
- argus/backend/service/testrun.py +489 -0
- argus/backend/service/user.py +308 -0
- argus/backend/service/views.py +219 -0
- argus/backend/service/views_widgets/__init__.py +0 -0
- argus/backend/service/views_widgets/graphed_stats.py +180 -0
- argus/backend/service/views_widgets/highlights.py +374 -0
- argus/backend/service/views_widgets/nemesis_stats.py +34 -0
- argus/backend/template_filters.py +27 -0
- argus/backend/tests/__init__.py +0 -0
- argus/backend/tests/client_service/__init__.py +0 -0
- argus/backend/tests/client_service/test_submit_results.py +79 -0
- argus/backend/tests/conftest.py +180 -0
- argus/backend/tests/results_service/__init__.py +0 -0
- argus/backend/tests/results_service/test_best_results.py +178 -0
- argus/backend/tests/results_service/test_cell.py +65 -0
- argus/backend/tests/results_service/test_chartjs_additional_functions.py +259 -0
- argus/backend/tests/results_service/test_create_chartjs.py +220 -0
- argus/backend/tests/results_service/test_result_metadata.py +100 -0
- argus/backend/tests/results_service/test_results_service.py +203 -0
- argus/backend/tests/results_service/test_validation_rules.py +213 -0
- argus/backend/tests/view_widgets/__init__.py +0 -0
- argus/backend/tests/view_widgets/test_highlights_api.py +532 -0
- argus/backend/util/common.py +65 -0
- argus/backend/util/config.py +38 -0
- argus/backend/util/encoders.py +56 -0
- argus/backend/util/logsetup.py +80 -0
- argus/backend/util/module_loaders.py +30 -0
- argus/backend/util/send_email.py +91 -0
- argus/client/base.py +1 -3
- argus/client/driver_matrix_tests/cli.py +17 -8
- argus/client/generic/cli.py +4 -2
- argus/client/generic/client.py +1 -0
- argus/client/generic_result.py +48 -9
- argus/client/sct/client.py +1 -3
- argus/client/sirenada/client.py +4 -1
- argus/client/tests/__init__.py +0 -0
- argus/client/tests/conftest.py +19 -0
- argus/client/tests/test_package.py +45 -0
- argus/client/tests/test_results.py +224 -0
- argus/common/sct_types.py +3 -0
- argus/common/sirenada_types.py +1 -1
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/METADATA +43 -19
- argus_alm-0.15.2.dist-info/RECORD +122 -0
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info}/WHEEL +2 -1
- argus_alm-0.15.2.dist-info/entry_points.txt +3 -0
- argus_alm-0.15.2.dist-info/top_level.txt +1 -0
- argus_alm-0.14.2.dist-info/RECORD +0 -20
- argus_alm-0.14.2.dist-info/entry_points.txt +0 -4
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.2.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from logging.config import dictConfig
|
|
3
|
+
from flask import has_request_context, request
|
|
4
|
+
|
|
5
|
+
LOG_FORMAT_REQUEST = "[%(levelcolor)s%(levelname)s%(colorreset)s] %(grey)s<%(remote_addr)s - %(url)s - %(endpoint)s>%(colorreset)s - %(module)s::%(funcName)s - %(message)s"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArgusRequestLogFormatter(logging.Formatter):
|
|
9
|
+
yellow = "\x1b[33;20m"
|
|
10
|
+
red = "\x1b[31;20m"
|
|
11
|
+
blue = "\x1b[34;20m"
|
|
12
|
+
bold_red = "\x1b[31;1m"
|
|
13
|
+
reset = "\x1b[0m"
|
|
14
|
+
grey = "\x1b[38;2;200;200;200m"
|
|
15
|
+
color_map = {
|
|
16
|
+
logging.DEBUG: grey,
|
|
17
|
+
logging.INFO: blue,
|
|
18
|
+
logging.WARNING: yellow,
|
|
19
|
+
logging.ERROR: red,
|
|
20
|
+
logging.CRITICAL: bold_red
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
24
|
+
record.grey = self.grey
|
|
25
|
+
record.colorreset = self.reset
|
|
26
|
+
record.levelcolor = self.color_map.get(record.levelno, self.grey)
|
|
27
|
+
if has_request_context():
|
|
28
|
+
record.url = request.url
|
|
29
|
+
record.remote_addr = request.remote_addr
|
|
30
|
+
record.endpoint = request.endpoint
|
|
31
|
+
else:
|
|
32
|
+
record.url = ''
|
|
33
|
+
record.remote_addr = ''
|
|
34
|
+
record.endpoint = ''
|
|
35
|
+
return super().format(record)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setup_application_logging(log_level=logging.INFO):
|
|
39
|
+
dictConfig({
|
|
40
|
+
'version': 1,
|
|
41
|
+
'formatters': {
|
|
42
|
+
'request': {
|
|
43
|
+
'class': f"{__name__}.{ArgusRequestLogFormatter.__name__}",
|
|
44
|
+
'format': LOG_FORMAT_REQUEST,
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
'handlers': {
|
|
48
|
+
'main': {
|
|
49
|
+
'class': 'logging.StreamHandler',
|
|
50
|
+
'stream': 'ext://sys.stderr',
|
|
51
|
+
'formatter': 'request'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
'loggers': {
|
|
55
|
+
'cassandra': {
|
|
56
|
+
'level': log_level,
|
|
57
|
+
'handlers': ['main']
|
|
58
|
+
},
|
|
59
|
+
'argus': {
|
|
60
|
+
'level': log_level,
|
|
61
|
+
'handlers': ['main']
|
|
62
|
+
},
|
|
63
|
+
'argus_backend': {
|
|
64
|
+
'level': log_level,
|
|
65
|
+
'handlers': ['main']
|
|
66
|
+
},
|
|
67
|
+
'werkzeug': {
|
|
68
|
+
'level': log_level,
|
|
69
|
+
'handlers': ['main']
|
|
70
|
+
},
|
|
71
|
+
'uwsgi': {
|
|
72
|
+
'level': log_level,
|
|
73
|
+
'handlers': ['main']
|
|
74
|
+
},
|
|
75
|
+
'__main__': {
|
|
76
|
+
'level': log_level,
|
|
77
|
+
'handlers': ['main']
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
})
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_filter(filter_name: str) -> Callable:
|
|
7
|
+
def outer_wrapper(func):
|
|
8
|
+
func.is_filter = True
|
|
9
|
+
func.filter_name = filter_name
|
|
10
|
+
|
|
11
|
+
@wraps(func)
|
|
12
|
+
def wrapper(*args, **kwargs):
|
|
13
|
+
return func(*args, **kwargs)
|
|
14
|
+
|
|
15
|
+
return wrapper
|
|
16
|
+
|
|
17
|
+
return outer_wrapper
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def export_functions(module_name: str, attr: str) -> list[Callable]:
|
|
21
|
+
module = sys.modules[module_name]
|
|
22
|
+
funcs = []
|
|
23
|
+
|
|
24
|
+
for member in dir(module):
|
|
25
|
+
export = getattr(module, member)
|
|
26
|
+
applicable_export = getattr(export, attr, False)
|
|
27
|
+
if applicable_export:
|
|
28
|
+
funcs.append(export)
|
|
29
|
+
|
|
30
|
+
return funcs
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import smtplib
|
|
2
|
+
from typing import List, Set
|
|
3
|
+
from smtplib import SMTPException
|
|
4
|
+
|
|
5
|
+
from email.mime.multipart import MIMEMultipart
|
|
6
|
+
from email.mime.text import MIMEText
|
|
7
|
+
from flask import current_app
|
|
8
|
+
from flask import render_template
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Email:
|
|
12
|
+
"""
|
|
13
|
+
Responsible for sending emails
|
|
14
|
+
"""
|
|
15
|
+
_attachments_size_limit = 10485760 # 10Mb = 20 * 1024 * 1024
|
|
16
|
+
_body_size_limit = 26214400 # 25Mb = 20 * 1024 * 1024
|
|
17
|
+
|
|
18
|
+
def __init__(self, init_connection=True):
|
|
19
|
+
self.sender: str = ""
|
|
20
|
+
self._password: str = ""
|
|
21
|
+
self._user: str = ""
|
|
22
|
+
self._server_host: str = ""
|
|
23
|
+
self._server_port: int = 0
|
|
24
|
+
self._connection: smtplib.SMTP | None = None
|
|
25
|
+
self._retrieve_credentials()
|
|
26
|
+
if init_connection:
|
|
27
|
+
self._connect()
|
|
28
|
+
|
|
29
|
+
def _retrieve_credentials(self):
|
|
30
|
+
self.sender = current_app.config["EMAIL_SENDER"]
|
|
31
|
+
self._password = current_app.config["EMAIL_SENDER_PASS"]
|
|
32
|
+
self._user = current_app.config["EMAIL_SENDER_USER"]
|
|
33
|
+
self._server_host = current_app.config["EMAIL_SERVER"]
|
|
34
|
+
self._server_port = int(current_app.config["EMAIL_SERVER_PORT"])
|
|
35
|
+
|
|
36
|
+
def _connect(self):
|
|
37
|
+
try:
|
|
38
|
+
self._connection = smtplib.SMTP(host=self._server_host, port=self._server_port)
|
|
39
|
+
self._connection.ehlo()
|
|
40
|
+
self._connection.starttls()
|
|
41
|
+
self._connection.login(user=self._user, password=self._password)
|
|
42
|
+
except SMTPException as details:
|
|
43
|
+
current_app.logger.error("Failed to initialize smtp session %s", details)
|
|
44
|
+
|
|
45
|
+
def _is_connection_open(self):
|
|
46
|
+
if not self._connection:
|
|
47
|
+
return False
|
|
48
|
+
try:
|
|
49
|
+
status, _ = self._connection.noop()
|
|
50
|
+
except SMTPException:
|
|
51
|
+
status = -1
|
|
52
|
+
|
|
53
|
+
return True if status == 250 else False
|
|
54
|
+
|
|
55
|
+
def _prepare_email(self, subject: str,
|
|
56
|
+
content: str,
|
|
57
|
+
recipients: List[str],
|
|
58
|
+
html: bool = True):
|
|
59
|
+
msg = MIMEMultipart()
|
|
60
|
+
msg['subject'] = subject
|
|
61
|
+
msg['from'] = self.sender
|
|
62
|
+
assert recipients, "No recipients provided"
|
|
63
|
+
msg['to'] = ','.join(recipients)
|
|
64
|
+
if html:
|
|
65
|
+
text_part = MIMEText(content, "html")
|
|
66
|
+
else:
|
|
67
|
+
text_part = MIMEText(content, "plain")
|
|
68
|
+
msg.attach(text_part)
|
|
69
|
+
email = msg.as_string()
|
|
70
|
+
return email
|
|
71
|
+
|
|
72
|
+
def send(self, subject, content, recipients, html=True):
|
|
73
|
+
"""
|
|
74
|
+
:param subject: text
|
|
75
|
+
:param content: text/html
|
|
76
|
+
:param recipients: iterable, list of recipients
|
|
77
|
+
:param html: True/False
|
|
78
|
+
:param files: paths of the files that will be attached to the email
|
|
79
|
+
:return:
|
|
80
|
+
"""
|
|
81
|
+
email = self._prepare_email(subject, content, recipients, html)
|
|
82
|
+
self._send_email(recipients, email)
|
|
83
|
+
|
|
84
|
+
def _send_email(self, recipients, email):
|
|
85
|
+
if not self._is_connection_open():
|
|
86
|
+
self._connect()
|
|
87
|
+
self._connection.sendmail(self.sender, recipients, email)
|
|
88
|
+
|
|
89
|
+
def __del__(self):
|
|
90
|
+
if self._connection:
|
|
91
|
+
self._connection.quit()
|
argus/client/base.py
CHANGED
|
@@ -22,7 +22,6 @@ class ArgusClient:
|
|
|
22
22
|
schema_version: str | None = None
|
|
23
23
|
|
|
24
24
|
class Routes():
|
|
25
|
-
# pylint: disable=too-few-public-methods
|
|
26
25
|
SUBMIT = "/testrun/$type/submit"
|
|
27
26
|
GET = "/testrun/$type/$id/get"
|
|
28
27
|
HEARTBEAT = "/testrun/$type/$id/heartbeat"
|
|
@@ -144,7 +143,7 @@ class ArgusClient:
|
|
|
144
143
|
if not (run_type and run_id):
|
|
145
144
|
raise ValueError("run_type and run_id must be set in func params or object attributes")
|
|
146
145
|
|
|
147
|
-
response = self.get(endpoint=self.Routes.GET, location_params={"type": run_type, "id": run_id
|
|
146
|
+
response = self.get(endpoint=self.Routes.GET, location_params={"type": run_type, "id": run_id})
|
|
148
147
|
self.check_response(response)
|
|
149
148
|
|
|
150
149
|
return response.json()["response"]
|
|
@@ -228,4 +227,3 @@ class ArgusClient:
|
|
|
228
227
|
}
|
|
229
228
|
)
|
|
230
229
|
self.check_response(response)
|
|
231
|
-
|
|
@@ -18,17 +18,22 @@ def cli():
|
|
|
18
18
|
|
|
19
19
|
def _submit_driver_result_internal(api_key: str, base_url: str, run_id: str, metadata_path: str, extra_headers: dict):
|
|
20
20
|
metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
|
|
21
|
-
LOGGER.info("Submitting results for %s [%s/%s] to Argus...", run_id,
|
|
21
|
+
LOGGER.info("Submitting results for %s [%s/%s] to Argus...", run_id,
|
|
22
|
+
metadata["driver_name"], metadata["driver_type"])
|
|
22
23
|
raw_xml = (Path(metadata_path).parent / metadata["junit_result"]).read_bytes()
|
|
23
24
|
client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url, extra_headers=extra_headers)
|
|
24
|
-
client.submit_driver_result(
|
|
25
|
+
client.submit_driver_result(
|
|
26
|
+
driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], raw_junit_data=base64.encodebytes(raw_xml))
|
|
25
27
|
LOGGER.info("Done.")
|
|
26
28
|
|
|
29
|
+
|
|
27
30
|
def _submit_driver_failure_internal(api_key: str, base_url: str, run_id: str, metadata_path: str, extra_headers: dict):
|
|
28
31
|
metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
|
|
29
|
-
LOGGER.info("Submitting failure for %s [%s/%s] to Argus...", run_id,
|
|
32
|
+
LOGGER.info("Submitting failure for %s [%s/%s] to Argus...", run_id,
|
|
33
|
+
metadata["driver_name"], metadata["driver_type"])
|
|
30
34
|
client = ArgusDriverMatrixClient(run_id=run_id, auth_token=api_key, base_url=base_url, extra_headers=extra_headers)
|
|
31
|
-
client.submit_driver_failure(
|
|
35
|
+
client.submit_driver_failure(
|
|
36
|
+
driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], failure_reason=metadata["failure_reason"])
|
|
32
37
|
LOGGER.info("Done.")
|
|
33
38
|
|
|
34
39
|
|
|
@@ -53,7 +58,8 @@ def submit_driver_matrix_run(api_key: str, base_url: str, run_id: str, build_id:
|
|
|
53
58
|
@click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
|
|
54
59
|
@click.option("--metadata-path", required=True, help="Path to the metadata .json file that contains path to junit xml and other required information")
|
|
55
60
|
def submit_driver_result(api_key: str, base_url: str, run_id: str, metadata_path: str, extra_headers: dict):
|
|
56
|
-
_submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
61
|
+
_submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
62
|
+
metadata_path=metadata_path, extra_headers=extra_headers)
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
@click.command("fail-driver")
|
|
@@ -63,7 +69,8 @@ def submit_driver_result(api_key: str, base_url: str, run_id: str, metadata_path
|
|
|
63
69
|
@click.option("--id", "run_id", required=True, help="UUID (v4 or v1) unique to the job")
|
|
64
70
|
@click.option("--metadata-path", required=True, help="Path to the metadata .json file that contains path to junit xml and other required information")
|
|
65
71
|
def submit_driver_failure(api_key: str, base_url: str, run_id: str, metadata_path: str, extra_headers: dict):
|
|
66
|
-
_submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
72
|
+
_submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
73
|
+
metadata_path=metadata_path, extra_headers=extra_headers)
|
|
67
74
|
|
|
68
75
|
|
|
69
76
|
@click.command("submit-or-fail-driver")
|
|
@@ -75,9 +82,11 @@ def submit_driver_failure(api_key: str, base_url: str, run_id: str, metadata_pat
|
|
|
75
82
|
def submit_or_fail_driver(api_key: str, base_url: str, run_id: str, metadata_path: str, extra_headers: dict):
|
|
76
83
|
metadata = json.loads(Path(metadata_path).read_text(encoding="utf-8"))
|
|
77
84
|
if metadata.get("failure_reason"):
|
|
78
|
-
_submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
85
|
+
_submit_driver_failure_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
86
|
+
metadata_path=metadata_path, extra_headers=extra_headers)
|
|
79
87
|
else:
|
|
80
|
-
_submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
88
|
+
_submit_driver_result_internal(api_key=api_key, base_url=base_url, run_id=run_id,
|
|
89
|
+
metadata_path=metadata_path, extra_headers=extra_headers)
|
|
81
90
|
|
|
82
91
|
|
|
83
92
|
@click.command("submit-env")
|
argus/client/generic/cli.py
CHANGED
|
@@ -9,6 +9,7 @@ from argus.client.generic.client import ArgusGenericClient
|
|
|
9
9
|
|
|
10
10
|
LOGGER = logging.getLogger(__name__)
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
def validate_extra_headers(ctx, param, value):
|
|
13
14
|
if isinstance(value, dict):
|
|
14
15
|
return value
|
|
@@ -36,7 +37,8 @@ def cli():
|
|
|
36
37
|
def submit_run(api_key: str, base_url: str, id: str, build_id: str, build_url: str, started_by: str, scylla_version: str = None, extra_headers: dict | None = None):
|
|
37
38
|
LOGGER.info("Submitting %s (%s) to Argus...", build_id, id)
|
|
38
39
|
client = ArgusGenericClient(auth_token=api_key, base_url=base_url, extra_headers=extra_headers)
|
|
39
|
-
client.submit_generic_run(build_id=build_id, run_id=id, started_by=started_by,
|
|
40
|
+
client.submit_generic_run(build_id=build_id, run_id=id, started_by=started_by,
|
|
41
|
+
build_url=build_url, scylla_version=scylla_version)
|
|
40
42
|
LOGGER.info("Done.")
|
|
41
43
|
|
|
42
44
|
|
|
@@ -68,7 +70,7 @@ def trigger_jobs(api_key: str, base_url: str, job_info_file: str, version: str,
|
|
|
68
70
|
LOGGER.error("File not found: %s", job_info_file)
|
|
69
71
|
exit(128)
|
|
70
72
|
payload = json.load(path.open("rt", encoding="utf-8"))
|
|
71
|
-
client.trigger_jobs({
|
|
73
|
+
client.trigger_jobs({"release": release, "version": version, "plan_id": plan_id, **payload})
|
|
72
74
|
|
|
73
75
|
|
|
74
76
|
cli.add_command(submit_run)
|
argus/client/generic/client.py
CHANGED
argus/client/generic_result.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from enum import Enum, auto
|
|
3
|
+
from functools import cached_property
|
|
3
4
|
from typing import Union
|
|
4
5
|
|
|
5
6
|
|
|
@@ -29,13 +30,15 @@ class ColumnMetadata:
|
|
|
29
30
|
unit: str
|
|
30
31
|
type: ResultType
|
|
31
32
|
higher_is_better: bool = None
|
|
33
|
+
visible: bool = True # controls visibility in UI, True by default
|
|
32
34
|
|
|
33
35
|
def as_dict(self) -> dict:
|
|
34
36
|
return {
|
|
35
37
|
"name": self.name,
|
|
36
38
|
"unit": self.unit,
|
|
37
39
|
"type": str(self.type),
|
|
38
|
-
"higher_is_better": self.higher_is_better
|
|
40
|
+
"higher_is_better": self.higher_is_better,
|
|
41
|
+
"visible": self.visible,
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
|
|
@@ -52,6 +55,7 @@ class ValidationRule:
|
|
|
52
55
|
"fixed_limit": self.fixed_limit
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
|
|
55
59
|
class ResultTableMeta(type):
|
|
56
60
|
def __new__(cls, name, bases, dct):
|
|
57
61
|
cls_instance = super().__new__(cls, name, bases, dct)
|
|
@@ -85,21 +89,39 @@ class Cell:
|
|
|
85
89
|
|
|
86
90
|
def as_dict(self) -> dict:
|
|
87
91
|
cell = {"value_text": self.value} if isinstance(self.value, str) else {"value": self.value}
|
|
88
|
-
cell.update({
|
|
89
|
-
"column": self.column,
|
|
90
|
-
"row": self.row,
|
|
91
|
-
"status": str(self.status)
|
|
92
|
-
})
|
|
92
|
+
cell.update({"column": self.column, "row": self.row, "status": str(self.status)})
|
|
93
93
|
return cell
|
|
94
94
|
|
|
95
95
|
|
|
96
96
|
@dataclass
|
|
97
|
-
class GenericResultTable
|
|
97
|
+
class GenericResultTable:
|
|
98
98
|
"""
|
|
99
99
|
Base class for all Generic Result Tables in Argus. Use it as a base class for your result table.
|
|
100
100
|
"""
|
|
101
|
-
|
|
101
|
+
|
|
102
|
+
name: str = ""
|
|
103
|
+
description: str = ""
|
|
104
|
+
columns: list[ColumnMetadata] = field(default_factory=list)
|
|
105
|
+
# automatic timestamp based on SUT version. Works only with SCT and refers to Scylla version.
|
|
106
|
+
sut_timestamp: int = 0
|
|
107
|
+
sut_package_name: str = ""
|
|
102
108
|
results: list[Cell] = field(default_factory=list)
|
|
109
|
+
validation_rules: dict[str, ValidationRule] = field(default_factory=dict)
|
|
110
|
+
|
|
111
|
+
@cached_property
|
|
112
|
+
def column_types(self):
|
|
113
|
+
"""Return columns types as a dictionary."""
|
|
114
|
+
return {column.name: column.type for column in self.columns}
|
|
115
|
+
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
"""Validate validation rules."""
|
|
118
|
+
for col_name, rule in self.validation_rules.items():
|
|
119
|
+
if col_name not in self.column_types:
|
|
120
|
+
raise ValueError(f"ValidationRule column {col_name} not found in the table")
|
|
121
|
+
if self.column_types[col_name] == ResultType.TEXT:
|
|
122
|
+
raise ValueError(f"Validation rules don't apply to TEXT columns")
|
|
123
|
+
if not isinstance(rule, ValidationRule):
|
|
124
|
+
raise ValueError(f"Validation rule for column {col_name} is not of type ValidationRule")
|
|
103
125
|
|
|
104
126
|
def as_dict(self) -> dict:
|
|
105
127
|
rows = []
|
|
@@ -118,7 +140,7 @@ class GenericResultTable(metaclass=ResultTableMeta):
|
|
|
118
140
|
return {
|
|
119
141
|
"meta": meta_info,
|
|
120
142
|
"sut_timestamp": self.sut_timestamp,
|
|
121
|
-
"results": [result.as_dict() for result in self.results]
|
|
143
|
+
"results": [result.as_dict() for result in self.results],
|
|
122
144
|
}
|
|
123
145
|
|
|
124
146
|
def add_result(self, column: str, row: str, value: Union[int, float, str], status: Status):
|
|
@@ -127,3 +149,20 @@ class GenericResultTable(metaclass=ResultTableMeta):
|
|
|
127
149
|
if isinstance(value, str) and self.column_types[column] != ResultType.TEXT:
|
|
128
150
|
raise ValueError(f"Column {column} is not of type TEXT")
|
|
129
151
|
self.results.append(Cell(column=column, row=row, value=value, status=status))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StaticGenericResultTable(GenericResultTable):
|
|
155
|
+
"""Results class for static results metainformation, defined in Meta class."""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self, name=None, description=None, columns=None, sut_package_name=None, validation_rules=None
|
|
159
|
+
):
|
|
160
|
+
meta = getattr(self.__class__, "Meta")
|
|
161
|
+
super().__init__(
|
|
162
|
+
name=name or meta.name,
|
|
163
|
+
description=description or meta.description,
|
|
164
|
+
columns=columns or getattr(meta, "Columns", getattr(meta, "columns", None)),
|
|
165
|
+
sut_package_name=sut_package_name or getattr(meta, "sut_package_name", ""),
|
|
166
|
+
validation_rules=validation_rules or getattr(
|
|
167
|
+
meta, "ValidationRules", getattr(meta, "validation_rules", {})),
|
|
168
|
+
)
|
argus/client/sct/client.py
CHANGED
|
@@ -18,7 +18,7 @@ class ArgusSCTClient(ArgusClient):
|
|
|
18
18
|
CREATE_RESOURCE = "/sct/$id/resource/create"
|
|
19
19
|
TERMINATE_RESOURCE = "/sct/$id/resource/$name/terminate"
|
|
20
20
|
UPDATE_RESOURCE = "/sct/$id/resource/$name/update"
|
|
21
|
-
SET_SCT_RUNNER
|
|
21
|
+
SET_SCT_RUNNER = "/sct/$id/sct_runner/set"
|
|
22
22
|
UPDATE_SHARDS_FOR_RESOURCE = "/sct/$id/resource/$name/shards"
|
|
23
23
|
SUBMIT_NEMESIS = "/sct/$id/nemesis/submit"
|
|
24
24
|
SUBMIT_GEMINI_RESULTS = "/sct/$id/gemini/submit"
|
|
@@ -209,7 +209,6 @@ class ArgusSCTClient(ArgusClient):
|
|
|
209
209
|
)
|
|
210
210
|
self.check_response(response)
|
|
211
211
|
|
|
212
|
-
|
|
213
212
|
def update_resource(self, name: str, update_data: dict[str, Any]) -> None:
|
|
214
213
|
"""
|
|
215
214
|
Update fields of the resource.
|
|
@@ -224,7 +223,6 @@ class ArgusSCTClient(ArgusClient):
|
|
|
224
223
|
)
|
|
225
224
|
self.check_response(response)
|
|
226
225
|
|
|
227
|
-
|
|
228
226
|
def submit_nemesis(self, name: str, class_name: str, start_time: int,
|
|
229
227
|
target_name: str, target_ip: str, target_shards: int) -> None:
|
|
230
228
|
"""
|
argus/client/sirenada/client.py
CHANGED
|
@@ -12,6 +12,7 @@ from argus.client.base import ArgusClient
|
|
|
12
12
|
|
|
13
13
|
LOGGER = logging.getLogger(__name__)
|
|
14
14
|
|
|
15
|
+
|
|
15
16
|
class SirenadaEnv(TypedDict):
|
|
16
17
|
SIRENADA_JOB_ID: str
|
|
17
18
|
SIRENADA_BROWSER: str
|
|
@@ -31,6 +32,7 @@ class TestCredentials(TypedDict):
|
|
|
31
32
|
ClusterID: str
|
|
32
33
|
region: str
|
|
33
34
|
|
|
35
|
+
|
|
34
36
|
class ArgusSirenadaClient(ArgusClient):
|
|
35
37
|
test_type = "sirenada"
|
|
36
38
|
schema_version: None = "v1"
|
|
@@ -138,7 +140,8 @@ class ArgusSirenadaClient(ArgusClient):
|
|
|
138
140
|
raise exc
|
|
139
141
|
|
|
140
142
|
credentials = self._read_credentials(self.results_path / self._credentials_filename)
|
|
141
|
-
results = self._parse_junit_results(junit_xml=self.results_path /
|
|
143
|
+
results = self._parse_junit_results(junit_xml=self.results_path /
|
|
144
|
+
self._junit_xml_filename, credentials=credentials, env=env)
|
|
142
145
|
request_body: RawSirenadaRequest = {}
|
|
143
146
|
|
|
144
147
|
request_body["run_id"] = env.get("SIRENADA_JOB_ID")
|
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
8
|
+
def test_dir():
|
|
9
|
+
return Path(__file__).parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture(scope="module", autouse=True)
|
|
13
|
+
def env_dir(test_dir):
|
|
14
|
+
env_dir = test_dir / 'test_env'
|
|
15
|
+
if env_dir.exists():
|
|
16
|
+
shutil.rmtree(env_dir)
|
|
17
|
+
yield env_dir
|
|
18
|
+
if env_dir.exists():
|
|
19
|
+
shutil.rmtree(env_dir)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import venv
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_command(command: list[str], cwd: str = None, env=None) -> subprocess.CompletedProcess:
|
|
9
|
+
result = subprocess.run(command, cwd=cwd, check=True, stdout=subprocess.PIPE,
|
|
10
|
+
stderr=subprocess.PIPE, text=True, env=env)
|
|
11
|
+
print(result.stdout)
|
|
12
|
+
return result
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture(scope='module', name='build_and_install')
|
|
16
|
+
def fixture_build_and_install(test_dir: Path, env_dir: Path):
|
|
17
|
+
"""Fixture to build and install the package."""
|
|
18
|
+
dist_dir = env_dir / 'dist'
|
|
19
|
+
|
|
20
|
+
# Build the package
|
|
21
|
+
run_command(['uv', 'build', '-o', str(dist_dir)], cwd=str(test_dir.parent.parent.parent))
|
|
22
|
+
|
|
23
|
+
package_path = next(dist_dir.glob("argus_alm-*-py3-none-any.whl"))
|
|
24
|
+
|
|
25
|
+
# install
|
|
26
|
+
run_command(['uv', 'tool', 'install', str(package_path)])
|
|
27
|
+
|
|
28
|
+
yield package_path
|
|
29
|
+
|
|
30
|
+
run_command(['uv', 'tool', 'uninstall', 'argus-alm'])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_should_import_installed_package(env_dir):
|
|
34
|
+
|
|
35
|
+
python_exec = ['uv', 'tool', 'run', '--from', 'argus-alm', 'python']
|
|
36
|
+
run_command(python_exec + ['-c', 'import argus.client; import argus.common; '
|
|
37
|
+
'from argus.client.sct.client import ArgusSCTClient'])
|
|
38
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
39
|
+
run_command(python_exec + ['-c', 'import argus.client.tests.test_package'])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_should_run_cli(build_and_install):
|
|
43
|
+
"""Test that the CLI can be run successfully."""
|
|
44
|
+
run_command(['uv', 'tool', 'run', '--from', 'argus-alm', 'argus-client-generic', '--help'])
|
|
45
|
+
run_command(['uv', 'tool', 'run', '--from', 'argus-alm', 'argus-driver-matrix-client', '--help'])
|