argus-alm 0.12.2__py3-none-any.whl → 0.12.4b1__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/backend/cli.py +1 -1
- argus/backend/controller/admin_api.py +26 -0
- argus/backend/controller/api.py +26 -1
- argus/backend/controller/main.py +21 -0
- argus/backend/controller/testrun_api.py +132 -1
- argus/backend/controller/view_api.py +162 -0
- argus/backend/models/web.py +16 -0
- argus/backend/plugins/core.py +28 -5
- argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
- argus/backend/plugins/driver_matrix_tests/model.py +252 -4
- argus/backend/plugins/driver_matrix_tests/raw_types.py +27 -0
- argus/backend/plugins/driver_matrix_tests/service.py +18 -0
- argus/backend/plugins/driver_matrix_tests/udt.py +14 -13
- argus/backend/plugins/generic/model.py +6 -3
- argus/backend/plugins/loader.py +2 -2
- argus/backend/plugins/sct/controller.py +31 -0
- argus/backend/plugins/sct/plugin.py +2 -1
- argus/backend/plugins/sct/service.py +101 -3
- argus/backend/plugins/sct/testrun.py +8 -2
- argus/backend/plugins/sct/types.py +18 -0
- argus/backend/plugins/sct/udt.py +6 -0
- argus/backend/plugins/sirenada/model.py +1 -1
- argus/backend/service/argus_service.py +116 -11
- argus/backend/service/build_system_monitor.py +37 -7
- argus/backend/service/jenkins_service.py +176 -1
- argus/backend/service/release_manager.py +14 -0
- argus/backend/service/stats.py +179 -21
- argus/backend/service/testrun.py +44 -5
- argus/backend/service/views.py +258 -0
- argus/backend/template_filters.py +7 -0
- argus/backend/util/common.py +14 -2
- argus/client/driver_matrix_tests/cli.py +110 -0
- argus/client/driver_matrix_tests/client.py +56 -193
- argus/client/sct/client.py +34 -0
- argus_alm-0.12.4b1.dist-info/METADATA +129 -0
- {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/RECORD +39 -36
- {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/WHEEL +1 -1
- {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/entry_points.txt +1 -0
- argus_alm-0.12.2.dist-info/METADATA +0 -206
- {argus_alm-0.12.2.dist-info → argus_alm-0.12.4b1.dist-info}/LICENSE +0 -0
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from functools import reduce
|
|
4
|
+
import logging
|
|
5
|
+
from pprint import pformat
|
|
4
6
|
import re
|
|
7
|
+
from typing import Literal, TypedDict
|
|
5
8
|
from uuid import UUID
|
|
9
|
+
from xml.etree import ElementTree
|
|
6
10
|
from cassandra.cqlengine import columns
|
|
7
11
|
from argus.backend.db import ScyllaCluster
|
|
8
12
|
from argus.backend.models.web import ArgusRelease
|
|
@@ -12,6 +16,8 @@ from argus.backend.plugins.driver_matrix_tests.raw_types import RawMatrixTestRes
|
|
|
12
16
|
from argus.backend.util.enums import TestStatus
|
|
13
17
|
|
|
14
18
|
|
|
19
|
+
LOGGER = logging.getLogger(__name__)
|
|
20
|
+
|
|
15
21
|
class DriverMatrixPluginError(Exception):
|
|
16
22
|
pass
|
|
17
23
|
|
|
@@ -26,6 +32,65 @@ class DriverMatrixRunSubmissionRequest():
|
|
|
26
32
|
matrix_results: list[RawMatrixTestResult]
|
|
27
33
|
|
|
28
34
|
|
|
35
|
+
@dataclass(init=True, repr=True, frozen=True)
|
|
36
|
+
class DriverMatrixRunSubmissionRequestV2():
|
|
37
|
+
schema_version: str
|
|
38
|
+
run_id: str
|
|
39
|
+
job_name: str
|
|
40
|
+
job_url: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
TestTypeType = Literal['java', 'cpp', 'python', 'gocql']
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AdaptedXUnitData(TypedDict):
|
|
47
|
+
timestamp: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def python_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
51
|
+
testsuites = list(xml.getroot().iter("testsuite"))
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"timestamp": testsuites[0].attrib.get("timestamp"),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def java_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
59
|
+
testsuites = xml.getroot()
|
|
60
|
+
ts_now = datetime.utcnow().timestamp()
|
|
61
|
+
try:
|
|
62
|
+
time_taken = float(testsuites.attrib.get("time"))
|
|
63
|
+
except ValueError:
|
|
64
|
+
time_taken = 0.0
|
|
65
|
+
|
|
66
|
+
timestamp = datetime.utcfromtimestamp(ts_now - time_taken).isoformat()
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"timestamp": timestamp,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cpp_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
74
|
+
testsuites = xml.getroot()
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"timestamp": testsuites.attrib.get("timestamp"),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def gocql_driver_matrix_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
82
|
+
testsuites = list(xml.getroot().iter("testsuite"))
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
"timestamp": testsuites[0].attrib.get("timestamp"),
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def generic_adapter(xml: ElementTree.ElementTree) -> AdaptedXUnitData:
|
|
90
|
+
return {
|
|
91
|
+
"timestamp": datetime.utcnow().isoformat()
|
|
92
|
+
}
|
|
93
|
+
|
|
29
94
|
class DriverTestRun(PluginModelBase):
|
|
30
95
|
_plugin_name = "driver-matrix-tests"
|
|
31
96
|
__table_name__ = "driver_test_run"
|
|
@@ -33,18 +98,28 @@ class DriverTestRun(PluginModelBase):
|
|
|
33
98
|
test_collection = columns.List(value_type=columns.UserDefinedType(user_type=TestCollection))
|
|
34
99
|
environment_info = columns.List(value_type=columns.UserDefinedType(user_type=EnvironmentInfo))
|
|
35
100
|
|
|
101
|
+
_no_upstream = ["rust"]
|
|
102
|
+
|
|
103
|
+
_TEST_ADAPTERS = {
|
|
104
|
+
"java": java_driver_matrix_adapter,
|
|
105
|
+
"cpp": cpp_driver_matrix_adapter,
|
|
106
|
+
"python": python_driver_matrix_adapter,
|
|
107
|
+
"gocql": gocql_driver_matrix_adapter,
|
|
108
|
+
}
|
|
109
|
+
|
|
36
110
|
|
|
37
111
|
_artifact_fnames = {
|
|
38
112
|
"cpp": r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.-]*)",
|
|
39
113
|
"gocql": r"xunit\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[v\d\.]*)",
|
|
40
114
|
"python": r"pytest\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[\d\.]*)",
|
|
41
115
|
"java": r"TEST-(?P<version>[\d\.\w-]*)",
|
|
116
|
+
"rust": r"(?P<driver_name>rust)_results_v(?P<version>[\d\w\-.]*)",
|
|
42
117
|
}
|
|
43
118
|
|
|
44
119
|
@classmethod
|
|
45
120
|
def _stats_query(cls) -> str:
|
|
46
121
|
return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
|
|
47
|
-
f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE
|
|
122
|
+
f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE build_id IN ? PER PARTITION LIMIT 15")
|
|
48
123
|
|
|
49
124
|
@classmethod
|
|
50
125
|
def get_distinct_product_versions(cls, release: ArgusRelease) -> list[str]:
|
|
@@ -76,6 +151,173 @@ class DriverTestRun(PluginModelBase):
|
|
|
76
151
|
|
|
77
152
|
@classmethod
|
|
78
153
|
def submit_run(cls, request_data: dict) -> 'DriverTestRun':
|
|
154
|
+
if request_data["schema_version"] == "v2":
|
|
155
|
+
req = DriverMatrixRunSubmissionRequestV2(**request_data)
|
|
156
|
+
else:
|
|
157
|
+
return cls.submit_matrix_run(request_data)
|
|
158
|
+
|
|
159
|
+
run = cls()
|
|
160
|
+
run.id = req.run_id
|
|
161
|
+
run.build_id = req.job_name
|
|
162
|
+
run.build_job_url = req.job_url
|
|
163
|
+
run.start_time = datetime.utcnow()
|
|
164
|
+
run.assign_categories()
|
|
165
|
+
try:
|
|
166
|
+
run.assignee = run.get_scheduled_assignee()
|
|
167
|
+
except Exception: # pylint: disable=broad-except
|
|
168
|
+
run.assignee = None
|
|
169
|
+
|
|
170
|
+
run.status = TestStatus.CREATED.value
|
|
171
|
+
run.save()
|
|
172
|
+
return run
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def submit_driver_result(cls, run_id: UUID, driver_name: str, driver_type: TestTypeType, xml_data: str):
|
|
176
|
+
run: DriverTestRun = cls.get(id=run_id)
|
|
177
|
+
|
|
178
|
+
collection = run.parse_result_xml(driver_name, xml_data, driver_type)
|
|
179
|
+
run.test_collection.append(collection)
|
|
180
|
+
|
|
181
|
+
if run.status == TestStatus.CREATED:
|
|
182
|
+
run.status = TestStatus.RUNNING.value
|
|
183
|
+
|
|
184
|
+
run.save()
|
|
185
|
+
return run
|
|
186
|
+
|
|
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(adapted_data["timestamp"][0:-1] if adapted_data["timestamp"][-1] == "Z" else adapted_data["timestamp"])
|
|
284
|
+
test_collection.name = name
|
|
285
|
+
test_collection.driver = driver_info.get("driver_name")
|
|
286
|
+
test_collection.tests_total = 0
|
|
287
|
+
test_collection.failures = 0
|
|
288
|
+
test_collection.errors = 0
|
|
289
|
+
test_collection.disabled = 0
|
|
290
|
+
test_collection.skipped = 0
|
|
291
|
+
test_collection.passed = 0
|
|
292
|
+
test_collection.time = 0.0
|
|
293
|
+
test_collection.suites = []
|
|
294
|
+
|
|
295
|
+
for xml_suite in testsuites.iter("testsuite"):
|
|
296
|
+
suite = TestSuite()
|
|
297
|
+
suite.name = xml_suite.attrib["name"]
|
|
298
|
+
suite.tests_total = int(xml_suite.attrib.get("tests", 0))
|
|
299
|
+
suite.failures = int(xml_suite.attrib.get("failures", 0))
|
|
300
|
+
suite.disabled = int(0)
|
|
301
|
+
suite.passed = self.get_passed_count(xml_suite.attrib)
|
|
302
|
+
suite.skipped = int(xml_suite.attrib.get("skipped", 0))
|
|
303
|
+
suite.errors = int(xml_suite.attrib.get("errors", 0))
|
|
304
|
+
suite.time = float(xml_suite.attrib["time"])
|
|
305
|
+
suite.cases = self.get_test_cases(xml_suite.findall("testcase"))
|
|
306
|
+
|
|
307
|
+
test_collection.suites.append(suite)
|
|
308
|
+
test_collection.tests_total += suite.tests_total
|
|
309
|
+
test_collection.failures += suite.failures
|
|
310
|
+
test_collection.errors += suite.errors
|
|
311
|
+
test_collection.disabled += suite.disabled
|
|
312
|
+
test_collection.skipped += suite.skipped
|
|
313
|
+
test_collection.passed += suite.passed
|
|
314
|
+
test_collection.time += suite.time
|
|
315
|
+
|
|
316
|
+
return test_collection
|
|
317
|
+
|
|
318
|
+
@classmethod
|
|
319
|
+
def submit_matrix_run(cls, request_data):
|
|
320
|
+
# Legacy method
|
|
79
321
|
req = DriverMatrixRunSubmissionRequest(**request_data)
|
|
80
322
|
run = cls()
|
|
81
323
|
run.id = req.run_id # pylint: disable=invalid-name
|
|
@@ -144,21 +386,26 @@ class DriverTestRun(PluginModelBase):
|
|
|
144
386
|
return []
|
|
145
387
|
|
|
146
388
|
def _determine_run_status(self):
|
|
389
|
+
for collection in self.test_collection:
|
|
390
|
+
# patch failure
|
|
391
|
+
if collection.failure_message:
|
|
392
|
+
return TestStatus.FAILED
|
|
393
|
+
|
|
147
394
|
if len(self.test_collection) < 2:
|
|
148
395
|
return TestStatus.FAILED
|
|
149
396
|
|
|
150
397
|
driver_types = {collection.driver for collection in self.test_collection}
|
|
151
|
-
if len(driver_types) <= 1:
|
|
398
|
+
if len(driver_types) <= 1 and not any(driver for driver in self._no_upstream if driver in driver_types):
|
|
152
399
|
return TestStatus.FAILED
|
|
153
400
|
|
|
154
|
-
failure_count = reduce(lambda acc, val: acc + (val.failures + val.errors), self.test_collection, 0)
|
|
401
|
+
failure_count = reduce(lambda acc, val: acc + (val.failures or 0 + val.errors or 0), self.test_collection, 0)
|
|
155
402
|
if failure_count > 0:
|
|
156
403
|
return TestStatus.FAILED
|
|
157
404
|
|
|
158
405
|
return TestStatus.PASSED
|
|
159
406
|
|
|
160
407
|
def change_status(self, new_status: TestStatus):
|
|
161
|
-
|
|
408
|
+
self.status = new_status
|
|
162
409
|
|
|
163
410
|
def get_events(self) -> list:
|
|
164
411
|
return []
|
|
@@ -168,6 +415,7 @@ class DriverTestRun(PluginModelBase):
|
|
|
168
415
|
|
|
169
416
|
def finish_run(self, payload: dict = None):
|
|
170
417
|
self.end_time = datetime.utcnow()
|
|
418
|
+
self.status = self._determine_run_status().value
|
|
171
419
|
|
|
172
420
|
def submit_logs(self, logs: list[dict]):
|
|
173
421
|
pass
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
1
2
|
from typing import TypedDict
|
|
3
|
+
from uuid import UUID
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class RawMatrixTestCase(TypedDict):
|
|
@@ -33,3 +35,28 @@ class RawMatrixTestResult(TypedDict):
|
|
|
33
35
|
time: float
|
|
34
36
|
timestamp: str
|
|
35
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
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import logging
|
|
3
|
+
from uuid import UUID
|
|
1
4
|
from argus.backend.db import ScyllaCluster
|
|
2
5
|
from argus.backend.models.web import ArgusRelease, ArgusTest
|
|
3
6
|
from argus.backend.plugins.driver_matrix_tests.model import DriverTestRun
|
|
4
7
|
|
|
5
8
|
|
|
9
|
+
LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
6
11
|
class DriverMatrixService:
|
|
7
12
|
def tested_versions_report(self, build_id: str) -> dict:
|
|
8
13
|
db = ScyllaCluster.get()
|
|
@@ -40,3 +45,16 @@ class DriverMatrixService:
|
|
|
40
45
|
"versions": version_map,
|
|
41
46
|
}
|
|
42
47
|
return response
|
|
48
|
+
|
|
49
|
+
def submit_driver_result(self, run_id: UUID | str, driver_name: str, driver_type: str, raw_xml: str) -> bool:
|
|
50
|
+
xml_data = base64.decodebytes(bytes(raw_xml, encoding="utf-8"))
|
|
51
|
+
DriverTestRun.submit_driver_result(UUID(run_id), driver_name, driver_type, xml_data)
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def submit_driver_failure(self, run_id: UUID | str, driver_name: str, driver_type: str, failure_reason: str) -> bool:
|
|
55
|
+
DriverTestRun.submit_driver_failure(UUID(run_id), driver_name, driver_type, failure_reason)
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def submit_env_info(self, run_id: UUID | str, raw_env: str) -> bool:
|
|
59
|
+
DriverTestRun.submit_env_info(UUID(run_id), raw_env)
|
|
60
|
+
return True
|
|
@@ -12,12 +12,12 @@ class TestCase(UserType):
|
|
|
12
12
|
|
|
13
13
|
class TestSuite(UserType):
|
|
14
14
|
name = columns.Text()
|
|
15
|
-
tests_total = columns.Integer()
|
|
16
|
-
failures = columns.Integer()
|
|
17
|
-
disabled = columns.Integer()
|
|
18
|
-
skipped = columns.Integer()
|
|
19
|
-
passed = columns.Integer()
|
|
20
|
-
errors = columns.Integer()
|
|
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
21
|
time = columns.Float()
|
|
22
22
|
cases = columns.List(value_type=columns.UserDefinedType(user_type=TestCase))
|
|
23
23
|
|
|
@@ -25,14 +25,15 @@ class TestSuite(UserType):
|
|
|
25
25
|
class TestCollection(UserType):
|
|
26
26
|
name = columns.Text()
|
|
27
27
|
driver = columns.Text()
|
|
28
|
-
tests_total = columns.Integer()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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)
|
|
34
35
|
timestamp = columns.DateTime()
|
|
35
|
-
time = columns.Float()
|
|
36
|
+
time = columns.Float(default=lambda: 0.0)
|
|
36
37
|
suites = columns.List(value_type=columns.UserDefinedType(user_type=TestSuite))
|
|
37
38
|
|
|
38
39
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
import re
|
|
2
3
|
from uuid import UUID
|
|
3
4
|
from cassandra.cqlengine import columns
|
|
4
5
|
from cassandra.cqlengine.models import Model
|
|
@@ -24,7 +25,7 @@ class GenericRun(PluginModelBase):
|
|
|
24
25
|
@classmethod
|
|
25
26
|
def _stats_query(cls) -> str:
|
|
26
27
|
return ("SELECT id, test_id, group_id, release_id, status, start_time, build_job_url, build_id, "
|
|
27
|
-
f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE
|
|
28
|
+
f"assignee, end_time, investigation_status, heartbeat, scylla_version FROM {cls.table_name()} WHERE build_id IN ? PER PARTITION LIMIT 15")
|
|
28
29
|
|
|
29
30
|
@classmethod
|
|
30
31
|
def get_distinct_product_versions(cls, release: ArgusRelease, cluster: ScyllaCluster = None) -> list[str]:
|
|
@@ -37,8 +38,10 @@ class GenericRun(PluginModelBase):
|
|
|
37
38
|
return sorted(list(unique_versions), reverse=True)
|
|
38
39
|
|
|
39
40
|
def submit_product_version(self, version: str):
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
pattern = re.compile(r"((?P<short>[\w.~]+)-(?P<build>(0\.)?(?P<date>[0-9]{8,8})\.(?P<commit>\w+).*))")
|
|
42
|
+
if match := pattern.search(version):
|
|
43
|
+
self.scylla_version = match.group("short")
|
|
44
|
+
self.set_full_version(version)
|
|
42
45
|
|
|
43
46
|
@classmethod
|
|
44
47
|
def load_test_run(cls, run_id: UUID) -> 'GenericRun':
|
argus/backend/plugins/loader.py
CHANGED
|
@@ -32,8 +32,8 @@ def plugin_loader() -> dict[str, PluginInfoBase]:
|
|
|
32
32
|
AVAILABLE_PLUGINS = plugin_loader()
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def all_plugin_models() -> list[PluginModelBase]:
|
|
36
|
-
return [model for plugin in AVAILABLE_PLUGINS.values() for model in plugin.all_models]
|
|
35
|
+
def all_plugin_models(include_all=False) -> list[PluginModelBase]:
|
|
36
|
+
return [model for plugin in AVAILABLE_PLUGINS.values() for model in plugin.all_models if issubclass(model, PluginModelBase) or include_all]
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def all_plugin_types():
|
|
@@ -81,6 +81,17 @@ def sct_resource_update_shards(run_id: str, resource_name: str):
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
@bp.route("/<string:run_id>/resource/<string:resource_name>/update", methods=["POST"])
|
|
85
|
+
@api_login_required
|
|
86
|
+
def sct_resource_update(run_id: str, resource_name: str):
|
|
87
|
+
payload = get_payload(request)
|
|
88
|
+
result = SCTService.update_resource(run_id=run_id, resource_name=resource_name, update_data=payload["update_data"])
|
|
89
|
+
return {
|
|
90
|
+
"status": "ok",
|
|
91
|
+
"response": result
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
84
95
|
@bp.route("/<string:run_id>/nemesis/submit", methods=["POST"])
|
|
85
96
|
@api_login_required
|
|
86
97
|
def sct_nemesis_submit(run_id: str):
|
|
@@ -152,3 +163,23 @@ def sct_get_kernel_report(release_name: str):
|
|
|
152
163
|
"status": "ok",
|
|
153
164
|
"response": result
|
|
154
165
|
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@bp.route("/<string:run_id>/junit/submit", methods=["POST"])
|
|
169
|
+
@api_login_required
|
|
170
|
+
def sct_submit_junit_report(run_id: str):
|
|
171
|
+
payload = get_payload(request)
|
|
172
|
+
result = SCTService.junit_submit(run_id, payload["file_name"], payload["content"])
|
|
173
|
+
return {
|
|
174
|
+
"status": "ok",
|
|
175
|
+
"response": result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
@bp.route("/<string:run_id>/junit/get_all", methods=["GET"])
|
|
179
|
+
@api_login_required
|
|
180
|
+
def sct_get_junit_reports(run_id: str):
|
|
181
|
+
result = SCTService.junit_get_all(run_id)
|
|
182
|
+
return {
|
|
183
|
+
"status": "ok",
|
|
184
|
+
"response": result
|
|
185
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from flask import Blueprint
|
|
2
2
|
|
|
3
|
-
from argus.backend.plugins.sct.testrun import SCTTestRun
|
|
3
|
+
from argus.backend.plugins.sct.testrun import SCTJunitReports, SCTTestRun
|
|
4
4
|
from argus.backend.plugins.sct.controller import bp as sct_bp
|
|
5
5
|
from argus.backend.plugins.core import PluginInfoBase, PluginModelBase
|
|
6
6
|
from argus.backend.plugins.sct.udt import (
|
|
@@ -23,6 +23,7 @@ class PluginInfo(PluginInfoBase):
|
|
|
23
23
|
controller: Blueprint = sct_bp
|
|
24
24
|
all_models = [
|
|
25
25
|
SCTTestRun,
|
|
26
|
+
SCTJunitReports,
|
|
26
27
|
]
|
|
27
28
|
all_types = [
|
|
28
29
|
NemesisRunInfo,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
2
4
|
from functools import reduce
|
|
3
5
|
import logging
|
|
4
6
|
import math
|
|
7
|
+
import re
|
|
5
8
|
from time import time
|
|
9
|
+
from xml.etree import ElementTree
|
|
6
10
|
from flask import g
|
|
7
11
|
from argus.backend.models.web import ArgusEventTypes
|
|
8
|
-
from argus.backend.plugins.sct.testrun import SCTTestRun, SubtestType
|
|
9
|
-
from argus.backend.plugins.sct.types import GeminiResultsRequest, PerformanceResultsRequest
|
|
12
|
+
from argus.backend.plugins.sct.testrun import SCTJunitReports, SCTTestRun, SubtestType
|
|
13
|
+
from argus.backend.plugins.sct.types import GeminiResultsRequest, PerformanceResultsRequest, ResourceUpdateRequest
|
|
10
14
|
from argus.backend.plugins.sct.udt import (
|
|
11
15
|
CloudInstanceDetails,
|
|
12
16
|
CloudResource,
|
|
@@ -60,7 +64,8 @@ class SCTService:
|
|
|
60
64
|
run: SCTTestRun = SCTTestRun.get(id=run_id)
|
|
61
65
|
for package_dict in packages:
|
|
62
66
|
package = PackageVersion(**package_dict)
|
|
63
|
-
run.packages
|
|
67
|
+
if package not in run.packages:
|
|
68
|
+
run.packages.append(package)
|
|
64
69
|
run.save()
|
|
65
70
|
except SCTTestRun.DoesNotExist as exception:
|
|
66
71
|
LOGGER.error("Run %s not found for SCTTestRun", run_id)
|
|
@@ -277,6 +282,33 @@ class SCTService:
|
|
|
277
282
|
|
|
278
283
|
return "updated"
|
|
279
284
|
|
|
285
|
+
@staticmethod
|
|
286
|
+
def update_resource(run_id: str, resource_name: str, update_data: ResourceUpdateRequest) -> str:
|
|
287
|
+
try:
|
|
288
|
+
fields_updated = {}
|
|
289
|
+
run: SCTTestRun = SCTTestRun.get(id=run_id)
|
|
290
|
+
resource = next(res for res in run.get_resources() if res.name == resource_name)
|
|
291
|
+
instance_info = update_data.pop("instance_info", None)
|
|
292
|
+
resource.state = ResourceState(update_data.get("state", resource.state)).value
|
|
293
|
+
if instance_info:
|
|
294
|
+
resource_instance_info = resource.get_instance_info()
|
|
295
|
+
for k, v in instance_info.items():
|
|
296
|
+
if k in resource_instance_info.keys():
|
|
297
|
+
resource_instance_info[k] = v
|
|
298
|
+
fields_updated[k] = v
|
|
299
|
+
run.save()
|
|
300
|
+
except StopIteration as exception:
|
|
301
|
+
LOGGER.error("Resource %s not found in run %s", resource_name, run_id)
|
|
302
|
+
raise SCTServiceException("Resource not found", resource_name) from exception
|
|
303
|
+
except SCTTestRun.DoesNotExist as exception:
|
|
304
|
+
LOGGER.error("Run %s not found for SCTTestRun", run_id)
|
|
305
|
+
raise SCTServiceException("Run not found", run_id) from exception
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"state": "updated",
|
|
309
|
+
"fields": fields_updated
|
|
310
|
+
}
|
|
311
|
+
|
|
280
312
|
@staticmethod
|
|
281
313
|
def terminate_resource(run_id: str, resource_name: str, reason: str) -> str:
|
|
282
314
|
try:
|
|
@@ -348,6 +380,8 @@ class SCTService:
|
|
|
348
380
|
wrapper = EventsBySeverity(severity=event.severity,
|
|
349
381
|
event_amount=event.total_events, last_events=event.messages)
|
|
350
382
|
run.get_events().append(wrapper)
|
|
383
|
+
coredumps = SCTService.locate_coredumps(run, run.get_events())
|
|
384
|
+
run.submit_logs(coredumps)
|
|
351
385
|
run.save()
|
|
352
386
|
except SCTTestRun.DoesNotExist as exception:
|
|
353
387
|
LOGGER.error("Run %s not found for SCTTestRun", run_id)
|
|
@@ -355,6 +389,39 @@ class SCTService:
|
|
|
355
389
|
|
|
356
390
|
return "added"
|
|
357
391
|
|
|
392
|
+
@staticmethod
|
|
393
|
+
def locate_coredumps(run: SCTTestRun, events: list[EventsBySeverity]) -> list[dict]:
|
|
394
|
+
flat_messages: list[str] = []
|
|
395
|
+
links = []
|
|
396
|
+
for es in events:
|
|
397
|
+
flat_messages.extend(es.last_events)
|
|
398
|
+
coredump_events = filter(lambda v: "coredumpevent" in v.lower(), flat_messages)
|
|
399
|
+
for idx, event in enumerate(coredump_events):
|
|
400
|
+
core_pattern = r"corefile_url=(?P<url>.+)$"
|
|
401
|
+
ts_pattern = r"^(?P<ts>\d{4}-\d{2}-\d{2} ([\d:]*)\.\d{3})"
|
|
402
|
+
node_name_pattern = r"node=(?P<name>.+)$"
|
|
403
|
+
core_url_match = re.search(core_pattern, event, re.MULTILINE)
|
|
404
|
+
node_name_match = re.search(node_name_pattern, event, re.MULTILINE)
|
|
405
|
+
ts_match = re.search(ts_pattern, event)
|
|
406
|
+
if core_url_match:
|
|
407
|
+
node_name = node_name_match.group("name") if node_name_match else f"unknown-node-{idx}"
|
|
408
|
+
split_name = node_name.split(" ")
|
|
409
|
+
node_name = split_name[1] if len(split_name) >= 2 else node_name
|
|
410
|
+
url = core_url_match.group("url")
|
|
411
|
+
timestamp_component = ""
|
|
412
|
+
if ts_match:
|
|
413
|
+
try:
|
|
414
|
+
timestamp = datetime.fromisoformat(ts_match.group("ts"))
|
|
415
|
+
timestamp_component = timestamp.strftime("-%Y-%m-%d_%H-%M-%S")
|
|
416
|
+
except ValueError:
|
|
417
|
+
pass
|
|
418
|
+
log_link = {
|
|
419
|
+
"log_name": f"core.scylla-{node_name}{timestamp_component}.gz",
|
|
420
|
+
"log_link": url
|
|
421
|
+
}
|
|
422
|
+
links.append(log_link)
|
|
423
|
+
return links
|
|
424
|
+
|
|
358
425
|
@staticmethod
|
|
359
426
|
def get_scylla_version_kernels_report(release_name: str):
|
|
360
427
|
all_release_runs = SCTTestRun.get_version_data_for_release(release_name=release_name)
|
|
@@ -391,3 +458,34 @@ class SCTService:
|
|
|
391
458
|
"versions": kernels_by_version,
|
|
392
459
|
"metadata": kernel_metadata
|
|
393
460
|
}
|
|
461
|
+
|
|
462
|
+
@staticmethod
|
|
463
|
+
def junit_submit(run_id: str, file_name: str, content: str) -> bool:
|
|
464
|
+
try:
|
|
465
|
+
report = SCTJunitReports.get(test_id=run_id, file_name=file_name)
|
|
466
|
+
if report:
|
|
467
|
+
raise SCTServiceException(f"Report {file_name} already exists.", file_name)
|
|
468
|
+
except SCTJunitReports.DoesNotExist:
|
|
469
|
+
pass
|
|
470
|
+
report = SCTJunitReports()
|
|
471
|
+
report.test_id = run_id
|
|
472
|
+
report.file_name = file_name
|
|
473
|
+
|
|
474
|
+
xml_content = str(base64.decodebytes(bytes(content, encoding="utf-8")), encoding="utf-8")
|
|
475
|
+
try:
|
|
476
|
+
_ = ElementTree.fromstring(xml_content)
|
|
477
|
+
except Exception:
|
|
478
|
+
raise SCTServiceException(f"Malformed JUnit report submitted")
|
|
479
|
+
|
|
480
|
+
report.report = xml_content
|
|
481
|
+
report.save()
|
|
482
|
+
|
|
483
|
+
return True
|
|
484
|
+
|
|
485
|
+
@staticmethod
|
|
486
|
+
def junit_get_all(run_id: str) -> list[SCTJunitReports]:
|
|
487
|
+
return list(SCTJunitReports.filter(test_id=run_id).all())
|
|
488
|
+
|
|
489
|
+
@staticmethod
|
|
490
|
+
def junit_get_single(run_id: str, file_name: str) -> SCTJunitReports:
|
|
491
|
+
return SCTJunitReports.get(test_id=run_id, file_name=file_name)
|