argus-alm 0.12.5__py3-none-any.whl → 0.12.8__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/controller/admin_api.py +67 -2
- argus/backend/controller/api.py +18 -1
- argus/backend/controller/auth.py +15 -13
- argus/backend/controller/client_api.py +10 -5
- argus/backend/controller/main.py +1 -0
- argus/backend/controller/testrun_api.py +2 -1
- argus/backend/models/result.py +40 -6
- argus/backend/plugins/core.py +3 -3
- argus/backend/plugins/driver_matrix_tests/controller.py +39 -0
- argus/backend/plugins/driver_matrix_tests/model.py +248 -2
- 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/sct/testrun.py +9 -3
- argus/backend/service/argus_service.py +15 -5
- argus/backend/service/build_system_monitor.py +3 -3
- argus/backend/service/client_service.py +22 -4
- argus/backend/service/jenkins_service.py +3 -1
- argus/backend/service/results_service.py +201 -0
- argus/backend/service/testrun.py +2 -19
- argus/backend/service/user.py +61 -4
- argus/backend/service/views.py +3 -3
- argus/backend/util/config.py +3 -1
- argus/backend/util/encoders.py +17 -0
- argus/client/base.py +18 -1
- argus/client/driver_matrix_tests/cli.py +110 -0
- argus/client/driver_matrix_tests/client.py +56 -193
- argus/client/generic_result.py +10 -5
- {argus_alm-0.12.5.dist-info → argus_alm-0.12.8.dist-info}/METADATA +1 -1
- {argus_alm-0.12.5.dist-info → argus_alm-0.12.8.dist-info}/RECORD +33 -31
- {argus_alm-0.12.5.dist-info → argus_alm-0.12.8.dist-info}/entry_points.txt +1 -0
- {argus_alm-0.12.5.dist-info → argus_alm-0.12.8.dist-info}/LICENSE +0 -0
- {argus_alm-0.12.5.dist-info → argus_alm-0.12.8.dist-info}/WHEEL +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
|
+
}
|
argus/backend/controller/api.py
CHANGED
|
@@ -4,7 +4,7 @@ import requests
|
|
|
4
4
|
from flask import (
|
|
5
5
|
Blueprint,
|
|
6
6
|
g,
|
|
7
|
-
request
|
|
7
|
+
request, Response
|
|
8
8
|
)
|
|
9
9
|
from flask.json import jsonify
|
|
10
10
|
from argus.backend.error_handlers import handle_api_exception
|
|
@@ -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,22 @@ def test_info():
|
|
|
381
382
|
"response": info
|
|
382
383
|
}
|
|
383
384
|
|
|
385
|
+
@bp.route("/test-results", methods=["GET", "HEAD"])
|
|
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
|
+
if request.method == 'HEAD':
|
|
393
|
+
exists = service.is_results_exist(test_id=UUID(test_id))
|
|
394
|
+
return Response(status=200 if exists else 404)
|
|
395
|
+
graphs, ticks = service.get_test_graphs(test_id=UUID(test_id))
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
"status": "ok",
|
|
399
|
+
"response": {"graphs": graphs, "ticks": ticks}
|
|
400
|
+
}
|
|
384
401
|
|
|
385
402
|
@bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
|
|
386
403
|
@api_login_required
|
argus/backend/controller/auth.py
CHANGED
|
@@ -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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
@@ -96,8 +105,4 @@ def run_finalize(run_type: str, run_id: str):
|
|
|
96
105
|
@api_login_required
|
|
97
106
|
def submit_results(run_type: str, run_id: str):
|
|
98
107
|
payload = get_payload(request)
|
|
99
|
-
|
|
100
|
-
return {
|
|
101
|
-
"status": "ok",
|
|
102
|
-
"response": result
|
|
103
|
-
}
|
|
108
|
+
return ClientService().submit_results(run_type=run_type, run_id=run_id, results=payload)
|
argus/backend/controller/main.py
CHANGED
|
@@ -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:
|
|
@@ -8,6 +8,7 @@ from flask import (
|
|
|
8
8
|
from argus.backend.error_handlers import handle_api_exception
|
|
9
9
|
from argus.backend.models.web import ArgusTest
|
|
10
10
|
from argus.backend.service.jenkins_service import JenkinsService
|
|
11
|
+
from argus.backend.service.results_service import ResultsService
|
|
11
12
|
from argus.backend.service.testrun import TestRunService
|
|
12
13
|
from argus.backend.service.user import api_login_required
|
|
13
14
|
from argus.backend.util.common import get_payload
|
|
@@ -67,7 +68,7 @@ def test_run_activity(run_id: str):
|
|
|
67
68
|
@bp.route("/run/<string:test_id>/<string:run_id>/fetch_results", methods=["GET"])
|
|
68
69
|
@api_login_required
|
|
69
70
|
def fetch_results(test_id: str, run_id: str):
|
|
70
|
-
tables =
|
|
71
|
+
tables = ResultsService().get_run_results(test_id=UUID(test_id), run_id=UUID(run_id))
|
|
71
72
|
return {
|
|
72
73
|
"status": "ok",
|
|
73
74
|
"tables": tables
|
argus/backend/models/result.py
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
from cassandra.cqlengine import columns
|
|
2
2
|
from cassandra.cqlengine.models import Model
|
|
3
3
|
from cassandra.cqlengine.usertype import UserType
|
|
4
|
-
from enum import Enum
|
|
5
4
|
|
|
5
|
+
class BestResult(UserType):
|
|
6
|
+
date = columns.DateTime()
|
|
7
|
+
value = columns.Double()
|
|
8
|
+
run_id = columns.UUID()
|
|
6
9
|
|
|
7
|
-
class
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
class ValidationRules(UserType):
|
|
11
|
+
higher_is_better = columns.Boolean()
|
|
12
|
+
margin_percent = columns.Double()
|
|
13
|
+
margin_value = columns.Double()
|
|
14
|
+
limit = columns.Double()
|
|
12
15
|
|
|
13
16
|
class ColumnMetadata(UserType):
|
|
14
17
|
name = columns.Ascii()
|
|
@@ -22,12 +25,42 @@ class ArgusGenericResultMetadata(Model):
|
|
|
22
25
|
name = columns.Text(required=True, primary_key=True)
|
|
23
26
|
description = columns.Text()
|
|
24
27
|
columns_meta = columns.List(value_type=columns.UserDefinedType(ColumnMetadata))
|
|
28
|
+
validation_rules = columns.Map(key_type=columns.Ascii(), value_type=columns.List(value_type=columns.UserDefinedType(ValidationRules)))
|
|
29
|
+
best_results = columns.Map(key_type=columns.Ascii(), value_type=columns.UserDefinedType(BestResult))
|
|
25
30
|
rows_meta = columns.List(value_type=columns.Ascii())
|
|
26
31
|
|
|
27
32
|
def __init__(self, **kwargs):
|
|
28
33
|
kwargs["columns_meta"] = [ColumnMetadata(**col) for col in kwargs.pop('columns_meta', [])]
|
|
34
|
+
kwargs["best_results"] = {k: [BestResult(**z) for z in v] for k, v in kwargs.pop('best_results', {}).items()}
|
|
35
|
+
kwargs["validation_rules"] = {k: ValidationRules(**v) for k, v in kwargs.pop('validation_rules', {}).items()}
|
|
29
36
|
super().__init__(**kwargs)
|
|
30
37
|
|
|
38
|
+
def update_if_changed(self, new_data: dict) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Updates table metadata if changed column/description or new rows were added.
|
|
41
|
+
See that rows can only be added, not removed once was sent.
|
|
42
|
+
Columns may be removed, but data in results table persists.
|
|
43
|
+
"""
|
|
44
|
+
updated = False
|
|
45
|
+
for field, value in new_data.items():
|
|
46
|
+
if field == "columns_meta":
|
|
47
|
+
value = [ColumnMetadata(**col) for col in value]
|
|
48
|
+
elif field == "rows_meta":
|
|
49
|
+
added_rows = []
|
|
50
|
+
for row in value:
|
|
51
|
+
if row not in self.rows_meta:
|
|
52
|
+
added_rows.append(row)
|
|
53
|
+
value = self.rows_meta + added_rows
|
|
54
|
+
elif field == "best_results":
|
|
55
|
+
value = {k: [BestResult(**z) for z in v] for k, v in value.items()}
|
|
56
|
+
elif field == "validation_rules":
|
|
57
|
+
value = {k: ValidationRules(**v) for k, v in value.items()}
|
|
58
|
+
if getattr(self, field) != value:
|
|
59
|
+
setattr(self, field, value)
|
|
60
|
+
updated = True
|
|
61
|
+
|
|
62
|
+
if updated:
|
|
63
|
+
self.save()
|
|
31
64
|
|
|
32
65
|
class ArgusGenericResultData(Model):
|
|
33
66
|
__table_name__ = "generic_result_data_v1"
|
|
@@ -38,4 +71,5 @@ class ArgusGenericResultData(Model):
|
|
|
38
71
|
row = columns.Ascii(primary_key=True, index=True)
|
|
39
72
|
sut_timestamp = columns.DateTime() # for sorting
|
|
40
73
|
value = columns.Double()
|
|
74
|
+
value_text = columns.Text()
|
|
41
75
|
status = columns.Ascii()
|
argus/backend/plugins/core.py
CHANGED
|
@@ -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.
|
|
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,
|
|
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=(
|
|
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
|
-
|
|
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
|