argus-alm 0.12.3__py3-none-any.whl → 0.12.4b2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. argus/backend/controller/admin_api.py +26 -0
  2. argus/backend/controller/api.py +26 -1
  3. argus/backend/controller/main.py +21 -0
  4. argus/backend/controller/testrun_api.py +16 -0
  5. argus/backend/controller/view_api.py +162 -0
  6. argus/backend/models/web.py +16 -0
  7. argus/backend/plugins/core.py +25 -10
  8. argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
  9. argus/backend/plugins/driver_matrix_tests/model.py +251 -3
  10. argus/backend/plugins/driver_matrix_tests/raw_types.py +27 -0
  11. argus/backend/plugins/driver_matrix_tests/service.py +18 -0
  12. argus/backend/plugins/driver_matrix_tests/udt.py +14 -13
  13. argus/backend/plugins/generic/model.py +5 -2
  14. argus/backend/plugins/sct/service.py +13 -1
  15. argus/backend/service/argus_service.py +116 -20
  16. argus/backend/service/build_system_monitor.py +37 -7
  17. argus/backend/service/jenkins_service.py +2 -1
  18. argus/backend/service/release_manager.py +14 -0
  19. argus/backend/service/stats.py +147 -11
  20. argus/backend/service/testrun.py +44 -5
  21. argus/backend/service/views.py +258 -0
  22. argus/backend/template_filters.py +7 -0
  23. argus/backend/util/common.py +14 -2
  24. argus/client/driver_matrix_tests/cli.py +110 -0
  25. argus/client/driver_matrix_tests/client.py +56 -193
  26. argus_alm-0.12.4b2.dist-info/METADATA +129 -0
  27. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/RECORD +30 -27
  28. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/entry_points.txt +1 -0
  29. argus_alm-0.12.3.dist-info/METADATA +0 -207
  30. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/LICENSE +0 -0
  31. {argus_alm-0.12.3.dist-info → argus_alm-0.12.4b2.dist-info}/WHEEL +0 -0
@@ -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,12 +98,22 @@ 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
@@ -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
- raise DriverMatrixPluginError("This method is obsolete. Status is now determined on submission.")
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
- failures = columns.Integer()
30
- disabled = columns.Integer()
31
- skipped = columns.Integer()
32
- passed = columns.Integer()
33
- errors = columns.Integer()
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
@@ -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
- self.scylla_version = version
41
- self.set_product_version(version)
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':
@@ -1,5 +1,6 @@
1
1
  import base64
2
2
  from dataclasses import dataclass
3
+ from datetime import datetime
3
4
  from functools import reduce
4
5
  import logging
5
6
  import math
@@ -397,14 +398,25 @@ class SCTService:
397
398
  coredump_events = filter(lambda v: "coredumpevent" in v.lower(), flat_messages)
398
399
  for idx, event in enumerate(coredump_events):
399
400
  core_pattern = r"corefile_url=(?P<url>.+)$"
401
+ ts_pattern = r"^(?P<ts>\d{4}-\d{2}-\d{2} ([\d:]*)\.\d{3})"
400
402
  node_name_pattern = r"node=(?P<name>.+)$"
401
403
  core_url_match = re.search(core_pattern, event, re.MULTILINE)
402
404
  node_name_match = re.search(node_name_pattern, event, re.MULTILINE)
405
+ ts_match = re.search(ts_pattern, event)
403
406
  if core_url_match:
404
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
405
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
406
418
  log_link = {
407
- "log_name": f"COREDUMP-{node_name}",
419
+ "log_name": f"core.scylla-{node_name}{timestamp_component}.gz",
408
420
  "log_link": url
409
421
  }
410
422
  links.append(log_link)