argus-alm 0.14.2__py3-none-any.whl → 0.15.1__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.1.dist-info}/METADATA +43 -19
- argus_alm-0.15.1.dist-info/RECORD +122 -0
- {argus_alm-0.14.2.dist-info → argus_alm-0.15.1.dist-info}/WHEEL +2 -1
- argus_alm-0.15.1.dist-info/entry_points.txt +3 -0
- argus_alm-0.15.1.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.1.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from flask import Blueprint, request
|
|
2
|
+
|
|
3
|
+
from argus.backend.error_handlers import handle_api_exception
|
|
4
|
+
from argus.backend.plugins.driver_matrix_tests.raw_types import DriverMatrixSubmitEnvRequest, DriverMatrixSubmitFailureRequest, DriverMatrixSubmitResultRequest
|
|
5
|
+
from argus.backend.service.user import api_login_required
|
|
6
|
+
from argus.backend.plugins.driver_matrix_tests.service import DriverMatrixService
|
|
7
|
+
from argus.backend.util.common import get_payload
|
|
8
|
+
|
|
9
|
+
bp = Blueprint("driver_matrix_api", __name__, url_prefix="/driver_matrix")
|
|
10
|
+
bp.register_error_handler(Exception, handle_api_exception)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@bp.route("/test_report", methods=["GET"])
|
|
14
|
+
@api_login_required
|
|
15
|
+
def driver_matrix_test_report():
|
|
16
|
+
|
|
17
|
+
build_id = request.args.get("buildId")
|
|
18
|
+
if not build_id:
|
|
19
|
+
raise Exception("No build id provided")
|
|
20
|
+
|
|
21
|
+
result = DriverMatrixService().tested_versions_report(build_id=build_id)
|
|
22
|
+
return {
|
|
23
|
+
"status": "ok",
|
|
24
|
+
"response": result
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@bp.route("/result/submit", methods=["POST"])
|
|
29
|
+
@api_login_required
|
|
30
|
+
def submit_result():
|
|
31
|
+
payload = get_payload(request)
|
|
32
|
+
request_data = DriverMatrixSubmitResultRequest(**payload)
|
|
33
|
+
|
|
34
|
+
result = DriverMatrixService().submit_driver_result(driver_name=request_data.driver_name,
|
|
35
|
+
driver_type=request_data.driver_type, run_id=request_data.run_id, raw_xml=request_data.raw_xml)
|
|
36
|
+
return {
|
|
37
|
+
"status": "ok",
|
|
38
|
+
"response": result
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@bp.route("/result/fail", methods=["POST"])
|
|
43
|
+
@api_login_required
|
|
44
|
+
def submit_failure():
|
|
45
|
+
payload = get_payload(request)
|
|
46
|
+
request_data = DriverMatrixSubmitFailureRequest(**payload)
|
|
47
|
+
|
|
48
|
+
result = DriverMatrixService().submit_driver_failure(driver_name=request_data.driver_name,
|
|
49
|
+
driver_type=request_data.driver_type, run_id=request_data.run_id, failure_reason=request_data.failure_reason)
|
|
50
|
+
return {
|
|
51
|
+
"status": "ok",
|
|
52
|
+
"response": result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@bp.route("/env/submit", methods=["POST"])
|
|
57
|
+
@api_login_required
|
|
58
|
+
def submit_env():
|
|
59
|
+
payload = get_payload(request)
|
|
60
|
+
request_data = DriverMatrixSubmitEnvRequest(**payload)
|
|
61
|
+
|
|
62
|
+
result = DriverMatrixService().submit_env_info(run_id=request_data.run_id, raw_env=request_data.raw_env)
|
|
63
|
+
return {
|
|
64
|
+
"status": "ok",
|
|
65
|
+
"response": result
|
|
66
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from functools import reduce
|
|
4
|
+
import logging
|
|
5
|
+
from pprint import pformat
|
|
6
|
+
import re
|
|
7
|
+
from typing import Literal, TypedDict
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
from xml.etree import ElementTree
|
|
10
|
+
from cassandra.cqlengine import columns
|
|
11
|
+
from cassandra.cqlengine.models import Model
|
|
12
|
+
from argus.backend.db import ScyllaCluster
|
|
13
|
+
from argus.backend.models.web import ArgusRelease
|
|
14
|
+
from argus.backend.plugins.core import PluginModelBase
|
|
15
|
+
from argus.backend.plugins.driver_matrix_tests.udt import TestCollection, TestSuite, TestCase, EnvironmentInfo
|
|
16
|
+
from argus.backend.plugins.driver_matrix_tests.raw_types import RawMatrixTestResult
|
|
17
|
+
from argus.common.enums import TestStatus
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
LOGGER = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DriverMatrixPluginError(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(init=True, repr=True, frozen=True)
|
|
28
|
+
class DriverMatrixRunSubmissionRequest():
|
|
29
|
+
schema_version: str
|
|
30
|
+
run_id: str
|
|
31
|
+
job_name: str
|
|
32
|
+
job_url: str
|
|
33
|
+
test_environment: dict[str, str]
|
|
34
|
+
matrix_results: list[RawMatrixTestResult]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(init=True, repr=True, frozen=True)
|
|
38
|
+
class DriverMatrixRunSubmissionRequestV2():
|
|
39
|
+
schema_version: str
|
|
40
|
+
run_id: str
|
|
41
|
+
job_name: str
|
|
42
|
+
job_url: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
TestTypeType = Literal['java', 'cpp', 'python', 'gocql']
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AdaptedXUnitData(TypedDict):
|
|
49
|
+
timestamp: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def python_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
53
|
+
testsuites = list(xml.getroot().iter("testsuite"))
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"timestamp": testsuites[0].attrib.get("timestamp"),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def java_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
61
|
+
testsuites = xml.getroot()
|
|
62
|
+
ts_now = datetime.utcnow().timestamp()
|
|
63
|
+
try:
|
|
64
|
+
time_taken = float(testsuites.attrib.get("time"))
|
|
65
|
+
except ValueError:
|
|
66
|
+
time_taken = 0.0
|
|
67
|
+
|
|
68
|
+
timestamp = datetime.utcfromtimestamp(ts_now - time_taken).isoformat()
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
"timestamp": timestamp,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def cpp_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
76
|
+
testsuites = xml.getroot()
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
"timestamp": testsuites.attrib.get("timestamp"),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def gocql_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
84
|
+
testsuites = list(xml.getroot().iter("testsuite"))
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"timestamp": testsuites[0].attrib.get("timestamp"),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def generic_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
92
|
+
return {
|
|
93
|
+
"timestamp": datetime.utcnow().isoformat()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DriverTestRun(PluginModelBase):
|
|
98
|
+
_plugin_name = "driver-matrix-tests"
|
|
99
|
+
__table_name__ = "driver_test_run"
|
|
100
|
+
scylla_version = columns.Text()
|
|
101
|
+
test_collection = columns.List(value_type=columns.UserDefinedType(user_type=TestCollection))
|
|
102
|
+
environment_info = columns.List(value_type=columns.UserDefinedType(user_type=EnvironmentInfo))
|
|
103
|
+
|
|
104
|
+
_no_upstream = ["rust"]
|
|
105
|
+
|
|
106
|
+
_TEST_ADAPTERS = {
|
|
107
|
+
"java": java_driver_matrix_adapter,
|
|
108
|
+
"cpp": cpp_driver_matrix_adapter,
|
|
109
|
+
"python": python_driver_matrix_adapter,
|
|
110
|
+
"gocql": gocql_driver_matrix_adapter,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_artifact_fnames = {
|
|
114
|
+
"cpp": r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.-]*)",
|
|
115
|
+
"gocql": r"xunit\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[v\d\.]*)",
|
|
116
|
+
"python": r"pytest\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[\d\.]*)",
|
|
117
|
+
"java": r"TEST-(?P<version>[\d\.\w-]*)",
|
|
118
|
+
"rust": r"(?P<driver_name>rust)_results_v(?P<version>[\d\w\-.]*)",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def _stats_query(cls) -> str:
|
|
123
|
+
return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
|
|
124
|
+
f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE build_id IN ? PER PARTITION LIMIT 15")
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def get_distinct_product_versions(cls, release: ArgusRelease) -> list[str]:
|
|
128
|
+
cluster = ScyllaCluster.get()
|
|
129
|
+
statement = cluster.prepare(f"SELECT scylla_version FROM {cls.table_name()} WHERE release_id = ?")
|
|
130
|
+
rows = cluster.session.execute(query=statement, parameters=(release.id,))
|
|
131
|
+
unique_versions = {r["scylla_version"] for r in rows if r["scylla_version"]}
|
|
132
|
+
|
|
133
|
+
return sorted(list(unique_versions), reverse=True)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def load_test_run(cls, run_id: UUID) -> 'DriverTestRun':
|
|
137
|
+
return cls.get(id=run_id)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def parse_driver_name(cls, raw_file_name: str) -> str:
|
|
141
|
+
for test, pattern in cls._artifact_fnames.items():
|
|
142
|
+
match = re.match(pattern, raw_file_name)
|
|
143
|
+
if not match:
|
|
144
|
+
continue
|
|
145
|
+
driver_info = match.groupdict()
|
|
146
|
+
if test == "java":
|
|
147
|
+
version = driver_info["version"]
|
|
148
|
+
return "scylla" if len(version.split(".")) > 3 or "scylla" in version else "datastax"
|
|
149
|
+
else:
|
|
150
|
+
return driver_info["driver_name"]
|
|
151
|
+
return "unknown_driver"
|
|
152
|
+
|
|
153
|
+
@classmethod
|
|
154
|
+
def submit_run(cls, request_data: dict) -> 'DriverTestRun':
|
|
155
|
+
if request_data["schema_version"] == "v2":
|
|
156
|
+
req = DriverMatrixRunSubmissionRequestV2(**request_data)
|
|
157
|
+
else:
|
|
158
|
+
return cls.submit_matrix_run(request_data)
|
|
159
|
+
|
|
160
|
+
run = cls()
|
|
161
|
+
run.id = req.run_id
|
|
162
|
+
run.build_id = req.job_name
|
|
163
|
+
run.build_job_url = req.job_url
|
|
164
|
+
run.start_time = datetime.utcnow()
|
|
165
|
+
run.assign_categories()
|
|
166
|
+
try:
|
|
167
|
+
run.assignee = run.get_scheduled_assignee()
|
|
168
|
+
except Exception:
|
|
169
|
+
run.assignee = None
|
|
170
|
+
|
|
171
|
+
run.status = TestStatus.CREATED.value
|
|
172
|
+
run.save()
|
|
173
|
+
return run
|
|
174
|
+
|
|
175
|
+
@classmethod
|
|
176
|
+
def submit_driver_result(cls, run_id: UUID, driver_name: str, driver_type: TestTypeType, xml_data: str):
|
|
177
|
+
run: DriverTestRun = cls.get(id=run_id)
|
|
178
|
+
|
|
179
|
+
collection = run.parse_result_xml(driver_name, xml_data, driver_type)
|
|
180
|
+
run.test_collection.append(collection)
|
|
181
|
+
|
|
182
|
+
if run.status == TestStatus.CREATED:
|
|
183
|
+
run.status = TestStatus.RUNNING.value
|
|
184
|
+
|
|
185
|
+
run.save()
|
|
186
|
+
return run
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def submit_driver_failure(cls, run_id: UUID, driver_name: str, driver_type: TestTypeType, fail_message: str):
|
|
190
|
+
run: DriverTestRun = cls.get(id=run_id)
|
|
191
|
+
|
|
192
|
+
collection = TestCollection()
|
|
193
|
+
collection.failures = 1
|
|
194
|
+
collection.failure_message = fail_message
|
|
195
|
+
collection.name = driver_name
|
|
196
|
+
driver_info = run.get_driver_info(driver_name, driver_type)
|
|
197
|
+
collection.driver = driver_info.get("driver_name")
|
|
198
|
+
collection.tests_total = 1
|
|
199
|
+
run.test_collection.append(collection)
|
|
200
|
+
|
|
201
|
+
if run.status == TestStatus.CREATED:
|
|
202
|
+
run.status = TestStatus.RUNNING.value
|
|
203
|
+
|
|
204
|
+
run.save()
|
|
205
|
+
return run
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def submit_env_info(cls, run_id: UUID, env_data: str):
|
|
209
|
+
run: DriverTestRun = cls.get(id=run_id)
|
|
210
|
+
env = run.parse_build_environment(env_data)
|
|
211
|
+
|
|
212
|
+
for key, value in env.items():
|
|
213
|
+
env_info = EnvironmentInfo()
|
|
214
|
+
env_info.key = key
|
|
215
|
+
env_info.value = value
|
|
216
|
+
run.environment_info.append(env_info)
|
|
217
|
+
|
|
218
|
+
run.scylla_version = env.get("scylla-version")
|
|
219
|
+
|
|
220
|
+
run.save()
|
|
221
|
+
return run
|
|
222
|
+
|
|
223
|
+
def parse_build_environment(self, raw_env: str) -> dict[str, str]:
|
|
224
|
+
result = {}
|
|
225
|
+
for line in raw_env.split("\n"):
|
|
226
|
+
if not line:
|
|
227
|
+
continue
|
|
228
|
+
LOGGER.debug("ENV: %s", line)
|
|
229
|
+
key, val = line.split(": ")
|
|
230
|
+
result[key] = val.strip()
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
|
|
234
|
+
def get_test_cases(self, cases: list[ElementTree.Element]) -> list[TestCase]:
|
|
235
|
+
result = []
|
|
236
|
+
for raw_case in cases:
|
|
237
|
+
children = list(raw_case.findall("./*"))
|
|
238
|
+
if len(children) > 0:
|
|
239
|
+
status = children[0].tag
|
|
240
|
+
message = f"{children[0].attrib.get('message', 'no-message')} ({children[0].attrib.get('type', 'no-type')})"
|
|
241
|
+
else:
|
|
242
|
+
status = "passed"
|
|
243
|
+
message = ""
|
|
244
|
+
|
|
245
|
+
case = TestCase()
|
|
246
|
+
case.name = raw_case.attrib["name"]
|
|
247
|
+
case.status = status
|
|
248
|
+
case.time = float(raw_case.attrib.get("time", 0.0))
|
|
249
|
+
case.classname = raw_case.attrib.get("classname", "")
|
|
250
|
+
case.message = message
|
|
251
|
+
result.append(case)
|
|
252
|
+
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
def get_driver_info(self, xml_name: str, test_type: TestTypeType) -> dict[str, str]:
|
|
256
|
+
if test_type == "cpp":
|
|
257
|
+
filename_re = r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.]*)(\.xml)?"
|
|
258
|
+
else:
|
|
259
|
+
filename_re = r"(?P<name>[\w]*)\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[\d\.]*)(\.xml)?"
|
|
260
|
+
|
|
261
|
+
match = re.match(filename_re, xml_name)
|
|
262
|
+
|
|
263
|
+
return match.groupdict() if match else {}
|
|
264
|
+
|
|
265
|
+
def get_passed_count(self, suite_attribs: dict[str, str]) -> int:
|
|
266
|
+
if (pass_count := suite_attribs.get("passed")):
|
|
267
|
+
return int(pass_count)
|
|
268
|
+
total = int(suite_attribs.get("tests", 0))
|
|
269
|
+
errors = int(suite_attribs.get("errors", 0))
|
|
270
|
+
skipped = int(suite_attribs.get("skipped", 0))
|
|
271
|
+
failures = int(suite_attribs.get("failures", 0))
|
|
272
|
+
|
|
273
|
+
return total - errors - skipped - failures
|
|
274
|
+
|
|
275
|
+
def parse_result_xml(self, name: str, xml_data: str, test_type: TestTypeType) -> TestCollection:
|
|
276
|
+
xml: ElementTree.ElementTree = ElementTree.ElementTree(ElementTree.fromstring(xml_data))
|
|
277
|
+
LOGGER.debug("%s", pformat(xml))
|
|
278
|
+
testsuites = xml.getroot()
|
|
279
|
+
adapted_data = self._TEST_ADAPTERS.get(test_type, generic_adapter)(xml)
|
|
280
|
+
|
|
281
|
+
driver_info = self.get_driver_info(name, test_type)
|
|
282
|
+
test_collection = TestCollection()
|
|
283
|
+
test_collection.timestamp = datetime.fromisoformat(
|
|
284
|
+
adapted_data["timestamp"][0:-1] if adapted_data["timestamp"][-1] == "Z" else adapted_data["timestamp"])
|
|
285
|
+
test_collection.name = name
|
|
286
|
+
test_collection.driver = driver_info.get("driver_name")
|
|
287
|
+
test_collection.tests_total = 0
|
|
288
|
+
test_collection.failures = 0
|
|
289
|
+
test_collection.errors = 0
|
|
290
|
+
test_collection.disabled = 0
|
|
291
|
+
test_collection.skipped = 0
|
|
292
|
+
test_collection.passed = 0
|
|
293
|
+
test_collection.time = 0.0
|
|
294
|
+
test_collection.suites = []
|
|
295
|
+
|
|
296
|
+
for xml_suite in testsuites.iter("testsuite"):
|
|
297
|
+
suite = TestSuite()
|
|
298
|
+
suite.name = xml_suite.attrib["name"]
|
|
299
|
+
suite.tests_total = int(xml_suite.attrib.get("tests", 0))
|
|
300
|
+
suite.failures = int(xml_suite.attrib.get("failures", 0))
|
|
301
|
+
suite.disabled = int(0)
|
|
302
|
+
suite.passed = self.get_passed_count(xml_suite.attrib)
|
|
303
|
+
suite.skipped = int(xml_suite.attrib.get("skipped", 0))
|
|
304
|
+
suite.errors = int(xml_suite.attrib.get("errors", 0))
|
|
305
|
+
suite.time = float(xml_suite.attrib["time"])
|
|
306
|
+
suite.cases = self.get_test_cases(xml_suite.findall("testcase"))
|
|
307
|
+
|
|
308
|
+
test_collection.suites.append(suite)
|
|
309
|
+
test_collection.tests_total += suite.tests_total
|
|
310
|
+
test_collection.failures += suite.failures
|
|
311
|
+
test_collection.errors += suite.errors
|
|
312
|
+
test_collection.disabled += suite.disabled
|
|
313
|
+
test_collection.skipped += suite.skipped
|
|
314
|
+
test_collection.passed += suite.passed
|
|
315
|
+
test_collection.time += suite.time
|
|
316
|
+
|
|
317
|
+
return test_collection
|
|
318
|
+
|
|
319
|
+
@classmethod
|
|
320
|
+
def submit_matrix_run(cls, request_data):
|
|
321
|
+
# Legacy method
|
|
322
|
+
req = DriverMatrixRunSubmissionRequest(**request_data)
|
|
323
|
+
run = cls()
|
|
324
|
+
run.id = req.run_id
|
|
325
|
+
run.build_id = req.job_name
|
|
326
|
+
run.build_job_url = req.job_url
|
|
327
|
+
run.assign_categories()
|
|
328
|
+
try:
|
|
329
|
+
run.assignee = run.get_scheduled_assignee()
|
|
330
|
+
except Exception:
|
|
331
|
+
run.assignee = None
|
|
332
|
+
for key, value in req.test_environment.items():
|
|
333
|
+
env_info = EnvironmentInfo()
|
|
334
|
+
env_info.key = key
|
|
335
|
+
env_info.value = value
|
|
336
|
+
run.environment_info.append(env_info)
|
|
337
|
+
|
|
338
|
+
run.scylla_version = req.test_environment.get("scylla-version")
|
|
339
|
+
run.test_collection = []
|
|
340
|
+
|
|
341
|
+
for result in req.matrix_results:
|
|
342
|
+
collection = TestCollection()
|
|
343
|
+
collection.name = result.get("name")
|
|
344
|
+
collection.driver = cls.parse_driver_name(collection.name)
|
|
345
|
+
collection.tests_total = result.get("tests")
|
|
346
|
+
collection.failures = result.get("failures")
|
|
347
|
+
collection.errors = result.get("errors")
|
|
348
|
+
collection.skipped = result.get("skipped")
|
|
349
|
+
collection.passed = result.get("passed")
|
|
350
|
+
collection.disabled = result.get("disabled")
|
|
351
|
+
collection.time = result.get("time")
|
|
352
|
+
timestamp = result.get("timestamp")
|
|
353
|
+
# TODO: Not needed once python>=3.11
|
|
354
|
+
timestamp = timestamp[0:-1] if timestamp[-1] == "Z" else timestamp
|
|
355
|
+
collection.timestamp = datetime.fromisoformat(timestamp)
|
|
356
|
+
for raw_suite in result.get("suites"):
|
|
357
|
+
suite = TestSuite()
|
|
358
|
+
suite.name = raw_suite.get("name")
|
|
359
|
+
suite.tests_total = raw_suite.get("tests")
|
|
360
|
+
suite.failures = raw_suite.get("failures")
|
|
361
|
+
suite.errors = raw_suite.get("errors")
|
|
362
|
+
suite.skipped = raw_suite.get("skipped")
|
|
363
|
+
suite.passed = raw_suite.get("passed")
|
|
364
|
+
suite.disabled = raw_suite.get("disabled")
|
|
365
|
+
suite.time = raw_suite.get("time")
|
|
366
|
+
|
|
367
|
+
for raw_case in raw_suite.get("cases"):
|
|
368
|
+
case = TestCase()
|
|
369
|
+
case.name = raw_case.get("name")
|
|
370
|
+
case.status = raw_case.get("status")
|
|
371
|
+
case.time = raw_case.get("time")
|
|
372
|
+
case.classname = raw_case.get("classname")
|
|
373
|
+
case.message = raw_case.get("message")
|
|
374
|
+
suite.cases.append(case)
|
|
375
|
+
|
|
376
|
+
collection.suites.append(suite)
|
|
377
|
+
run.test_collection.append(collection)
|
|
378
|
+
|
|
379
|
+
run.status = run._determine_run_status().value
|
|
380
|
+
run.save()
|
|
381
|
+
return run
|
|
382
|
+
|
|
383
|
+
def get_resources(self) -> list:
|
|
384
|
+
return []
|
|
385
|
+
|
|
386
|
+
def get_nemeses(self) -> list:
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
def _determine_run_status(self):
|
|
390
|
+
for collection in self.test_collection:
|
|
391
|
+
# patch failure
|
|
392
|
+
if collection.failure_message:
|
|
393
|
+
return TestStatus.FAILED
|
|
394
|
+
|
|
395
|
+
if len(self.test_collection) < 2:
|
|
396
|
+
return TestStatus.FAILED
|
|
397
|
+
|
|
398
|
+
driver_types = {collection.driver for collection in self.test_collection}
|
|
399
|
+
if len(driver_types) <= 1 and not any(driver for driver in self._no_upstream if driver in driver_types):
|
|
400
|
+
return TestStatus.FAILED
|
|
401
|
+
|
|
402
|
+
failure_count = reduce(lambda acc, val: acc + (val.failures or 0 + val.errors or 0), self.test_collection, 0)
|
|
403
|
+
if failure_count > 0:
|
|
404
|
+
return TestStatus.FAILED
|
|
405
|
+
|
|
406
|
+
return TestStatus.PASSED
|
|
407
|
+
|
|
408
|
+
def change_status(self, new_status: TestStatus):
|
|
409
|
+
self.status = new_status
|
|
410
|
+
|
|
411
|
+
def get_events(self) -> list:
|
|
412
|
+
return []
|
|
413
|
+
|
|
414
|
+
def submit_product_version(self, version: str):
|
|
415
|
+
self.scylla_version = version
|
|
416
|
+
try:
|
|
417
|
+
new_assignee = self.get_assignment(version)
|
|
418
|
+
except Model.DoesNotExist:
|
|
419
|
+
new_assignee = None
|
|
420
|
+
if new_assignee:
|
|
421
|
+
self.assignee = new_assignee
|
|
422
|
+
|
|
423
|
+
def finish_run(self, payload: dict = None):
|
|
424
|
+
self.end_time = datetime.utcnow()
|
|
425
|
+
status = payload.get("status", "passed")
|
|
426
|
+
self.status = TestStatus(status).value
|
|
427
|
+
|
|
428
|
+
def submit_logs(self, logs: list[dict]):
|
|
429
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from flask import Blueprint
|
|
2
|
+
|
|
3
|
+
from argus.backend.plugins.core import PluginInfoBase, PluginModelBase
|
|
4
|
+
from argus.backend.plugins.driver_matrix_tests.model import DriverTestRun
|
|
5
|
+
from argus.backend.plugins.driver_matrix_tests.controller import bp as driver_matrix_api_bp
|
|
6
|
+
from argus.backend.plugins.driver_matrix_tests.udt import TestCollection, EnvironmentInfo, TestCase, TestSuite
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PluginInfo(PluginInfoBase):
|
|
10
|
+
name: str = "driver-matrix-tests"
|
|
11
|
+
model: PluginModelBase = DriverTestRun
|
|
12
|
+
controller: Blueprint = driver_matrix_api_bp
|
|
13
|
+
all_models = [
|
|
14
|
+
DriverTestRun,
|
|
15
|
+
]
|
|
16
|
+
all_types = [
|
|
17
|
+
TestCollection,
|
|
18
|
+
TestSuite,
|
|
19
|
+
TestCase,
|
|
20
|
+
EnvironmentInfo
|
|
21
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RawMatrixTestCase(TypedDict):
|
|
7
|
+
name: str
|
|
8
|
+
status: str
|
|
9
|
+
time: float
|
|
10
|
+
classname: str
|
|
11
|
+
message: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RawMatrixTestSuite(TypedDict):
|
|
15
|
+
name: str
|
|
16
|
+
tests: int
|
|
17
|
+
failures: int
|
|
18
|
+
disabled: int
|
|
19
|
+
skipped: int
|
|
20
|
+
passed: int
|
|
21
|
+
errors: int
|
|
22
|
+
time: float
|
|
23
|
+
cases: list[RawMatrixTestCase]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RawMatrixTestResult(TypedDict):
|
|
27
|
+
name: str
|
|
28
|
+
driver_name: str
|
|
29
|
+
tests: int
|
|
30
|
+
failures: int
|
|
31
|
+
errors: int
|
|
32
|
+
disabled: int
|
|
33
|
+
skipped: int
|
|
34
|
+
passed: int
|
|
35
|
+
time: float
|
|
36
|
+
timestamp: str
|
|
37
|
+
suites: list[RawMatrixTestSuite]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(init=True, frozen=True)
|
|
41
|
+
class DriverMatrixSubmitResultRequest():
|
|
42
|
+
schema_version: str
|
|
43
|
+
run_id: UUID
|
|
44
|
+
driver_type: str
|
|
45
|
+
driver_name: str
|
|
46
|
+
raw_xml: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(init=True, frozen=True)
|
|
50
|
+
class DriverMatrixSubmitFailureRequest():
|
|
51
|
+
schema_version: str
|
|
52
|
+
run_id: UUID
|
|
53
|
+
driver_type: str
|
|
54
|
+
driver_name: str
|
|
55
|
+
failure_reason: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(init=True, frozen=True)
|
|
59
|
+
class DriverMatrixSubmitEnvRequest():
|
|
60
|
+
schema_version: str
|
|
61
|
+
run_id: UUID
|
|
62
|
+
raw_env: str
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from argus.backend.db import ScyllaCluster
|
|
5
|
+
from argus.backend.models.web import ArgusRelease, ArgusTest
|
|
6
|
+
from argus.backend.plugins.driver_matrix_tests.model import DriverTestRun
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DriverMatrixService:
|
|
13
|
+
def tested_versions_report(self, build_id: str) -> dict:
|
|
14
|
+
db = ScyllaCluster.get()
|
|
15
|
+
all_runs_for_test_query = db.prepare(f"SELECT * FROM {DriverTestRun.table_name()} WHERE build_id = ?")
|
|
16
|
+
|
|
17
|
+
rows = list(db.session.execute(all_runs_for_test_query, parameters=(build_id,)).all())
|
|
18
|
+
|
|
19
|
+
if len(rows) == 0:
|
|
20
|
+
raise Exception(f"No results for build_id {build_id}", build_id)
|
|
21
|
+
|
|
22
|
+
latest = rows[0]
|
|
23
|
+
try:
|
|
24
|
+
test: ArgusTest = ArgusTest.get(id=latest["test_id"])
|
|
25
|
+
release: ArgusRelease = ArgusRelease.get(id=latest["release_id"])
|
|
26
|
+
except (ArgusTest.DoesNotExist, ArgusRelease.DoesNotExist):
|
|
27
|
+
raise Exception(
|
|
28
|
+
f"Unable to find release and test information for build_id {build_id} and run_id {latest['id']}", build_id, latest["id"])
|
|
29
|
+
|
|
30
|
+
version_map = {}
|
|
31
|
+
|
|
32
|
+
for row in rows:
|
|
33
|
+
driver_versions = [(col["name"], col["driver"]) for col in row["test_collection"]]
|
|
34
|
+
for version, driver_type in driver_versions:
|
|
35
|
+
driver_type = driver_type if driver_type else "Unknown"
|
|
36
|
+
versions: list = version_map.get(driver_type, [])
|
|
37
|
+
if version not in versions:
|
|
38
|
+
versions.append(version)
|
|
39
|
+
versions.sort()
|
|
40
|
+
version_map[driver_type] = versions
|
|
41
|
+
|
|
42
|
+
response = {
|
|
43
|
+
"release": release.name,
|
|
44
|
+
"test": test.name,
|
|
45
|
+
"build_id": build_id,
|
|
46
|
+
"versions": version_map,
|
|
47
|
+
}
|
|
48
|
+
return response
|
|
49
|
+
|
|
50
|
+
def submit_driver_result(self, run_id: UUID | str, driver_name: str, driver_type: str, raw_xml: str) -> bool:
|
|
51
|
+
xml_data = base64.decodebytes(bytes(raw_xml, encoding="utf-8"))
|
|
52
|
+
DriverTestRun.submit_driver_result(UUID(run_id), driver_name, driver_type, xml_data)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
def submit_driver_failure(self, run_id: UUID | str, driver_name: str, driver_type: str, failure_reason: str) -> bool:
|
|
56
|
+
DriverTestRun.submit_driver_failure(UUID(run_id), driver_name, driver_type, failure_reason)
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def submit_env_info(self, run_id: UUID | str, raw_env: str) -> bool:
|
|
60
|
+
DriverTestRun.submit_env_info(UUID(run_id), raw_env)
|
|
61
|
+
return True
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from cassandra.cqlengine.usertype import UserType
|
|
2
|
+
from cassandra.cqlengine import columns
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestCase(UserType):
|
|
6
|
+
name = columns.Text()
|
|
7
|
+
status = columns.Text()
|
|
8
|
+
time = columns.Float()
|
|
9
|
+
classname = columns.Text()
|
|
10
|
+
message = columns.Text()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestSuite(UserType):
|
|
14
|
+
name = columns.Text()
|
|
15
|
+
tests_total = columns.Integer(default=lambda: 0)
|
|
16
|
+
failures = columns.Integer(default=lambda: 0)
|
|
17
|
+
disabled = columns.Integer(default=lambda: 0)
|
|
18
|
+
skipped = columns.Integer(default=lambda: 0)
|
|
19
|
+
passed = columns.Integer(default=lambda: 0)
|
|
20
|
+
errors = columns.Integer(default=lambda: 0)
|
|
21
|
+
time = columns.Float()
|
|
22
|
+
cases = columns.List(value_type=columns.UserDefinedType(user_type=TestCase))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCollection(UserType):
|
|
26
|
+
name = columns.Text()
|
|
27
|
+
driver = columns.Text()
|
|
28
|
+
tests_total = columns.Integer(default=lambda: 0)
|
|
29
|
+
failure_message = columns.Text()
|
|
30
|
+
failures = columns.Integer(default=lambda: 0)
|
|
31
|
+
disabled = columns.Integer(default=lambda: 0)
|
|
32
|
+
skipped = columns.Integer(default=lambda: 0)
|
|
33
|
+
passed = columns.Integer(default=lambda: 0)
|
|
34
|
+
errors = columns.Integer(default=lambda: 0)
|
|
35
|
+
timestamp = columns.DateTime()
|
|
36
|
+
time = columns.Float(default=lambda: 0.0)
|
|
37
|
+
suites = columns.List(value_type=columns.UserDefinedType(user_type=TestSuite))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class EnvironmentInfo(UserType):
|
|
41
|
+
key = columns.Text()
|
|
42
|
+
value = columns.Text()
|