argus-alm 0.12.5__py3-none-any.whl → 0.12.7__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 (32) hide show
  1. argus/backend/controller/admin_api.py +67 -2
  2. argus/backend/controller/api.py +14 -0
  3. argus/backend/controller/auth.py +15 -13
  4. argus/backend/controller/client_api.py +9 -0
  5. argus/backend/controller/main.py +1 -0
  6. argus/backend/models/result.py +22 -0
  7. argus/backend/plugins/core.py +3 -3
  8. argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
  9. argus/backend/plugins/driver_matrix_tests/model.py +248 -2
  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/sct/testrun.py +2 -0
  14. argus/backend/service/argus_service.py +15 -5
  15. argus/backend/service/build_system_monitor.py +3 -3
  16. argus/backend/service/client_service.py +13 -1
  17. argus/backend/service/jenkins_service.py +3 -1
  18. argus/backend/service/results_service.py +140 -0
  19. argus/backend/service/testrun.py +13 -8
  20. argus/backend/service/user.py +61 -4
  21. argus/backend/service/views.py +3 -3
  22. argus/backend/util/config.py +3 -1
  23. argus/backend/util/encoders.py +17 -0
  24. argus/client/base.py +18 -1
  25. argus/client/driver_matrix_tests/cli.py +110 -0
  26. argus/client/driver_matrix_tests/client.py +56 -193
  27. {argus_alm-0.12.5.dist-info → argus_alm-0.12.7.dist-info}/METADATA +1 -1
  28. {argus_alm-0.12.5.dist-info → argus_alm-0.12.7.dist-info}/RECORD +31 -30
  29. {argus_alm-0.12.5.dist-info → argus_alm-0.12.7.dist-info}/WHEEL +1 -1
  30. {argus_alm-0.12.5.dist-info → argus_alm-0.12.7.dist-info}/entry_points.txt +1 -0
  31. argus/client/generic_result_old.py +0 -143
  32. {argus_alm-0.12.5.dist-info → argus_alm-0.12.7.dist-info}/LICENSE +0 -0
@@ -7,8 +7,8 @@ from flask import (
7
7
  )
8
8
  from argus.backend.error_handlers import handle_api_exception
9
9
  from argus.backend.service.release_manager import ReleaseEditPayload, ReleaseManagerService
10
- from argus.backend.service.user import api_login_required, check_roles
11
- from argus.backend.models.web import UserRoles
10
+ from argus.backend.service.user import UserService, api_login_required, check_roles
11
+ from argus.backend.models.web import User, UserRoles
12
12
 
13
13
  bp = Blueprint('admin_api', __name__, url_prefix='/api/v1')
14
14
  LOGGER = logging.getLogger(__name__)
@@ -287,3 +287,68 @@ def quick_toggle_group_enabled():
287
287
  "status": "ok",
288
288
  "response": res
289
289
  }
290
+
291
+
292
+ @bp.route("/users", methods=["GET"])
293
+ @check_roles(UserRoles.Admin)
294
+ @api_login_required
295
+ def user_info():
296
+ result = UserService().get_users_privileged()
297
+
298
+ return {
299
+ "status": "ok",
300
+ "response": result
301
+ }
302
+
303
+
304
+ @bp.route("/user/<string:user_id>/email/set", methods=["POST"])
305
+ @check_roles(UserRoles.Admin)
306
+ @api_login_required
307
+ def user_change_email(user_id: str):
308
+ payload = get_payload(request)
309
+
310
+ user = User.get(id=user_id)
311
+ result = UserService().update_email(user=user, new_email=payload["newEmail"])
312
+
313
+ return {
314
+ "status": "ok",
315
+ "response": result
316
+ }
317
+
318
+
319
+ @bp.route("/user/<string:user_id>/delete", methods=["POST"])
320
+ @check_roles(UserRoles.Admin)
321
+ @api_login_required
322
+ def user_delete(user_id: str):
323
+ result = UserService().delete_user(user_id=user_id)
324
+
325
+ return {
326
+ "status": "ok",
327
+ "response": result
328
+ }
329
+
330
+
331
+ @bp.route("/user/<string:user_id>/password/set", methods=["POST"])
332
+ @check_roles(UserRoles.Admin)
333
+ @api_login_required
334
+ def user_change_password(user_id: str):
335
+ payload = get_payload(request)
336
+
337
+ user = User.get(id=user_id)
338
+ result = UserService().update_password(user=user, old_password="", new_password=payload["newPassword"], force=True)
339
+
340
+ return {
341
+ "status": "ok",
342
+ "response": result
343
+ }
344
+
345
+ @bp.route("/user/<string:user_id>/admin/toggle", methods=["POST"])
346
+ @check_roles(UserRoles.Admin)
347
+ @api_login_required
348
+ def user_toggle_admin(user_id: str):
349
+ result = UserService().toggle_admin(user_id=user_id)
350
+
351
+ return {
352
+ "status": "ok",
353
+ "response": result
354
+ }
@@ -14,6 +14,7 @@ from argus.backend.controller.testrun_api import bp as testrun_bp
14
14
  from argus.backend.controller.team import bp as team_bp
15
15
  from argus.backend.controller.view_api import bp as view_bp
16
16
  from argus.backend.service.argus_service import ArgusService, ScheduleUpdateRequest
17
+ from argus.backend.service.results_service import ResultsService
17
18
  from argus.backend.service.user import UserService, api_login_required
18
19
  from argus.backend.service.stats import ReleaseStatsCollector
19
20
  from argus.backend.models.web import ArgusRelease, ArgusGroup, ArgusTest, User, UserOauthToken
@@ -381,6 +382,19 @@ def test_info():
381
382
  "response": info
382
383
  }
383
384
 
385
+ @bp.route("/test-results", methods=["GET"])
386
+ @api_login_required
387
+ def test_results():
388
+ test_id = request.args.get("testId")
389
+ if not test_id:
390
+ raise Exception("No testId provided")
391
+ service = ResultsService()
392
+ info = service.get_results(test_id=UUID(test_id))
393
+
394
+ return {
395
+ "status": "ok",
396
+ "response": info
397
+ }
384
398
 
385
399
  @bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
386
400
  @api_login_required
@@ -5,7 +5,7 @@ from flask import (
5
5
  )
6
6
  from werkzeug.security import check_password_hash
7
7
  from argus.backend.models.web import User
8
- from argus.backend.service.user import UserService, load_logged_in_user, login_required
8
+ from argus.backend.service.user import UserService, UserServiceException, load_logged_in_user, login_required
9
9
 
10
10
  bp = Blueprint('auth', __name__, url_prefix='/auth')
11
11
 
@@ -21,24 +21,26 @@ def login():
21
21
  session["csrf_token"] = token
22
22
 
23
23
  if request.method == 'POST':
24
- username = request.form["username"]
25
- password = request.form["password"]
26
- error = None
27
24
  try:
28
- user = User.get(username=username)
29
- except User.DoesNotExist:
30
- user = None
25
+ if "password" not in current_app.config.get("LOGIN_METHODS", []):
26
+ raise UserServiceException("Password Login is disabled")
27
+ username = request.form["username"]
28
+ password = request.form["password"]
31
29
 
32
- if not user:
33
- error = "User not found"
34
- elif not check_password_hash(user.password, password):
35
- error = "Incorrect Password"
30
+ try:
31
+ user: User = User.get(username=username)
32
+ except User.DoesNotExist:
33
+ raise UserServiceException("User not found")
34
+
35
+ if not check_password_hash(user.password, password):
36
+ raise UserServiceException("Incorrect Password")
36
37
 
37
- if not error:
38
38
  session.clear()
39
39
  session["user_id"] = str(user.id)
40
40
  session["csrf_token"] = token
41
- flash(error, category="error")
41
+ except UserServiceException as exc:
42
+ flash(next(iter(exc.args), "No message"), category="error")
43
+
42
44
  return redirect(url_for('main.home'))
43
45
 
44
46
  return render_template('auth/login.html.j2',
@@ -23,6 +23,15 @@ def submit_run(run_type: str):
23
23
  "response": result
24
24
  }
25
25
 
26
+ @bp.route("/testrun/<string:run_type>/<string:run_id>/get", methods=["GET"])
27
+ @api_login_required
28
+ def get_run(run_type: str, run_id: str):
29
+ result = ClientService().get_run(run_type=run_type, run_id=run_id)
30
+ return {
31
+ "status": "ok",
32
+ "response": result
33
+ }
34
+
26
35
 
27
36
  @bp.route("/testrun/<string:run_type>/<string:run_id>/heartbeat", methods=["POST"])
28
37
  @api_login_required
@@ -152,6 +152,7 @@ def profile_oauth_github_callback():
152
152
  try:
153
153
  first_run_info = service.github_callback(req_code)
154
154
  except Exception as exc: # pylint: disable=broad-except
155
+ LOGGER.error("An error occured in callback", exc_info=True)
155
156
  flash(message=exc.args[0], category="error")
156
157
  return redirect(url_for("main.error", type=403))
157
158
  if first_run_info:
@@ -28,6 +28,28 @@ class ArgusGenericResultMetadata(Model):
28
28
  kwargs["columns_meta"] = [ColumnMetadata(**col) for col in kwargs.pop('columns_meta', [])]
29
29
  super().__init__(**kwargs)
30
30
 
31
+ def update_if_changed(self, new_data: dict) -> None:
32
+ """
33
+ Updates table metadata if changed column/description or new rows were added.
34
+ See that rows can only be added, not removed once was sent.
35
+ Columns may be removed, but data in results table persists.
36
+ """
37
+ updated = False
38
+ for field, value in new_data.items():
39
+ if field == "columns_meta":
40
+ value = [ColumnMetadata(**col) for col in value]
41
+ elif field == "rows_meta":
42
+ added_rows = []
43
+ for row in value:
44
+ if row not in self.rows_meta:
45
+ added_rows.append(row)
46
+ value = self.rows_meta + added_rows
47
+ if getattr(self, field) != value:
48
+ setattr(self, field, value)
49
+ updated = True
50
+
51
+ if updated:
52
+ self.save()
31
53
 
32
54
  class ArgusGenericResultData(Model):
33
55
  __table_name__ = "generic_result_data_v1"
@@ -30,7 +30,7 @@ class PluginModelBase(Model):
30
30
  _plugin_name = "unknown"
31
31
  # Metadata
32
32
  build_id = columns.Text(required=True, partition_key=True)
33
- start_time = columns.DateTime(required=True, primary_key=True, clustering_order="DESC", default=datetime.now, custom_index=True)
33
+ start_time = columns.DateTime(required=True, primary_key=True, clustering_order="DESC", default=datetime.utcnow, custom_index=True)
34
34
  id = columns.UUID(index=True, required=True)
35
35
  release_id = columns.UUID(index=True)
36
36
  group_id = columns.UUID(index=True)
@@ -110,11 +110,11 @@ class PluginModelBase(Model):
110
110
  return assignees_uuids[0] if len(assignees_uuids) > 0 else None
111
111
 
112
112
  @classmethod
113
- def get_jobs_assigned_to_user(cls, user: User):
113
+ def get_jobs_assigned_to_user(cls, user_id: str | UUID):
114
114
  cluster = ScyllaCluster.get()
115
115
  query = cluster.prepare("SELECT build_id, start_time, release_id, group_id, assignee, "
116
116
  f"test_id, id, status, investigation_status, build_job_url, scylla_version FROM {cls.table_name()} WHERE assignee = ?")
117
- rows = cluster.session.execute(query=query, parameters=(user.id,))
117
+ rows = cluster.session.execute(query=query, parameters=(user_id,))
118
118
 
119
119
  return list(rows)
120
120
 
@@ -1,8 +1,10 @@
1
1
  from flask import Blueprint, request
2
2
 
3
3
  from argus.backend.error_handlers import handle_api_exception
4
+ from argus.backend.plugins.driver_matrix_tests.raw_types import DriverMatrixSubmitEnvRequest, DriverMatrixSubmitFailureRequest, DriverMatrixSubmitResultRequest
4
5
  from argus.backend.service.user import api_login_required
5
6
  from argus.backend.plugins.driver_matrix_tests.service import DriverMatrixService
7
+ from argus.backend.util.common import get_payload
6
8
 
7
9
  bp = Blueprint("driver_matrix_api", __name__, url_prefix="/driver_matrix")
8
10
  bp.register_error_handler(Exception, handle_api_exception)
@@ -22,3 +24,40 @@ def driver_matrix_test_report():
22
24
  "status": "ok",
23
25
  "response": result
24
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, driver_type=request_data.driver_type, run_id=request_data.run_id, raw_xml=request_data.raw_xml)
35
+ return {
36
+ "status": "ok",
37
+ "response": result
38
+ }
39
+
40
+
41
+ @bp.route("/result/fail", methods=["POST"])
42
+ @api_login_required
43
+ def submit_failure():
44
+ payload = get_payload(request)
45
+ request_data = DriverMatrixSubmitFailureRequest(**payload)
46
+
47
+ result = DriverMatrixService().submit_driver_failure(driver_name=request_data.driver_name, driver_type=request_data.driver_type, run_id=request_data.run_id, failure_reason=request_data.failure_reason)
48
+ return {
49
+ "status": "ok",
50
+ "response": result
51
+ }
52
+
53
+ @bp.route("/env/submit", methods=["POST"])
54
+ @api_login_required
55
+ def submit_env():
56
+ payload = get_payload(request)
57
+ request_data = DriverMatrixSubmitEnvRequest(**payload)
58
+
59
+ result = DriverMatrixService().submit_env_info(run_id=request_data.run_id, raw_env=request_data.raw_env)
60
+ return {
61
+ "status": "ok",
62
+ "response": result
63
+ }
@@ -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"
@@ -35,6 +100,14 @@ class DriverTestRun(PluginModelBase):
35
100
 
36
101
  _no_upstream = ["rust"]
37
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
+
110
+
38
111
  _artifact_fnames = {
39
112
  "cpp": r"TEST-(?P<driver_name>[\w]*)-(?P<version>[\d\.-]*)",
40
113
  "gocql": r"xunit\.(?P<driver_name>[\w]*)\.(?P<proto>v\d)\.(?P<version>[v\d\.]*)",
@@ -78,6 +151,173 @@ class DriverTestRun(PluginModelBase):
78
151
 
79
152
  @classmethod
80
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
81
321
  req = DriverMatrixRunSubmissionRequest(**request_data)
82
322
  run = cls()
83
323
  run.id = req.run_id # pylint: disable=invalid-name
@@ -146,6 +386,11 @@ class DriverTestRun(PluginModelBase):
146
386
  return []
147
387
 
148
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
+
149
394
  if len(self.test_collection) < 2:
150
395
  return TestStatus.FAILED
151
396
 
@@ -153,14 +398,14 @@ class DriverTestRun(PluginModelBase):
153
398
  if len(driver_types) <= 1 and not any(driver for driver in self._no_upstream if driver in driver_types):
154
399
  return TestStatus.FAILED
155
400
 
156
- 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)
157
402
  if failure_count > 0:
158
403
  return TestStatus.FAILED
159
404
 
160
405
  return TestStatus.PASSED
161
406
 
162
407
  def change_status(self, new_status: TestStatus):
163
- raise DriverMatrixPluginError("This method is obsolete. Status is now determined on submission.")
408
+ self.status = new_status
164
409
 
165
410
  def get_events(self) -> list:
166
411
  return []
@@ -170,6 +415,7 @@ class DriverTestRun(PluginModelBase):
170
415
 
171
416
  def finish_run(self, payload: dict = None):
172
417
  self.end_time = datetime.utcnow()
418
+ self.status = self._determine_run_status().value
173
419
 
174
420
  def submit_logs(self, logs: list[dict]):
175
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