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,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, metadata["driver_name"], metadata["driver_type"])
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(driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], raw_junit_data=base64.encodebytes(raw_xml))
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, metadata["driver_name"], metadata["driver_type"])
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(driver_name=metadata["driver_name"], driver_type=metadata["driver_type"], failure_reason=metadata["failure_reason"])
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, metadata_path=metadata_path, extra_headers=extra_headers)
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, metadata_path=metadata_path, extra_headers=extra_headers)
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, metadata_path=metadata_path, extra_headers=extra_headers)
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, metadata_path=metadata_path, extra_headers=extra_headers)
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")
@@ -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, build_url=build_url, scylla_version=scylla_version)
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({ "release": release, "version": version, "plan_id": plan_id, **payload })
73
+ client.trigger_jobs({"release": release, "version": version, "plan_id": plan_id, **payload})
72
74
 
73
75
 
74
76
  cli.add_command(submit_run)
@@ -4,6 +4,7 @@ from argus.client.base import ArgusClient
4
4
 
5
5
  LOGGER = logging.getLogger(__name__)
6
6
 
7
+
7
8
  class ArgusGenericClient(ArgusClient):
8
9
  test_type = "generic"
9
10
  schema_version: None = "v1"
@@ -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(metaclass=ResultTableMeta):
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
- sut_timestamp: int = 0 # automatic timestamp based on SUT version. Works only with SCT and refers to Scylla version.
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
+ )
@@ -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 = "/sct/$id/sct_runner/set"
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
  """
@@ -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 / self._junit_xml_filename, credentials=credentials, env=env)
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'])