argus-alm 0.12.7__py3-none-any.whl → 0.12.9__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 +3 -1
- argus/backend/controller/api.py +7 -4
- argus/backend/controller/client_api.py +1 -5
- argus/backend/controller/testrun_api.py +2 -1
- argus/backend/db.py +1 -1
- argus/backend/models/result.py +85 -10
- argus/backend/models/web.py +2 -1
- argus/backend/plugins/loader.py +1 -1
- argus/backend/plugins/sct/testrun.py +10 -3
- argus/backend/service/client_service.py +31 -10
- argus/backend/service/notification_manager.py +4 -2
- argus/backend/service/results_service.py +189 -12
- argus/backend/service/testrun.py +16 -22
- argus/backend/tests/__init__.py +0 -0
- argus/backend/tests/argus_web.test.yaml +39 -0
- argus/backend/tests/conftest.py +44 -0
- argus/backend/tests/results_service/__init__.py +0 -0
- argus/backend/tests/results_service/test_best_results.py +70 -0
- argus/client/generic_result.py +36 -7
- argus/client/generic_result_old.py +143 -0
- {argus_alm-0.12.7.dist-info → argus_alm-0.12.9.dist-info}/METADATA +1 -1
- {argus_alm-0.12.7.dist-info → argus_alm-0.12.9.dist-info}/RECORD +25 -19
- {argus_alm-0.12.7.dist-info → argus_alm-0.12.9.dist-info}/WHEEL +1 -1
- {argus_alm-0.12.7.dist-info → argus_alm-0.12.9.dist-info}/LICENSE +0 -0
- {argus_alm-0.12.7.dist-info → argus_alm-0.12.9.dist-info}/entry_points.txt +0 -0
argus/backend/cli.py
CHANGED
|
@@ -15,6 +15,9 @@ LOGGER = logging.getLogger(__name__)
|
|
|
15
15
|
@click.command('sync-models')
|
|
16
16
|
@with_appcontext
|
|
17
17
|
def sync_models_command():
|
|
18
|
+
sync_models()
|
|
19
|
+
|
|
20
|
+
def sync_models():
|
|
18
21
|
cluster = ScyllaCluster.get()
|
|
19
22
|
cluster.sync_core_tables()
|
|
20
23
|
LOGGER.info("Synchronizing plugin types...")
|
|
@@ -29,7 +32,6 @@ def sync_models_command():
|
|
|
29
32
|
LOGGER.info("Plugins ready.")
|
|
30
33
|
click.echo("All models synchronized.")
|
|
31
34
|
|
|
32
|
-
|
|
33
35
|
@cli_bp.cli.add_command
|
|
34
36
|
@click.command('scan-jenkins')
|
|
35
37
|
@with_appcontext
|
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
|
|
@@ -382,18 +382,21 @@ def test_info():
|
|
|
382
382
|
"response": info
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
@bp.route("/test-results", methods=["GET"])
|
|
385
|
+
@bp.route("/test-results", methods=["GET", "HEAD"])
|
|
386
386
|
@api_login_required
|
|
387
387
|
def test_results():
|
|
388
388
|
test_id = request.args.get("testId")
|
|
389
389
|
if not test_id:
|
|
390
390
|
raise Exception("No testId provided")
|
|
391
391
|
service = ResultsService()
|
|
392
|
-
|
|
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))
|
|
393
396
|
|
|
394
397
|
return {
|
|
395
398
|
"status": "ok",
|
|
396
|
-
"response":
|
|
399
|
+
"response": {"graphs": graphs, "ticks": ticks}
|
|
397
400
|
}
|
|
398
401
|
|
|
399
402
|
@bp.route("/test_run/comment/get", methods=["GET"]) # TODO: remove
|
|
@@ -105,8 +105,4 @@ def run_finalize(run_type: str, run_id: str):
|
|
|
105
105
|
@api_login_required
|
|
106
106
|
def submit_results(run_type: str, run_id: str):
|
|
107
107
|
payload = get_payload(request)
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
"status": "ok",
|
|
111
|
-
"response": result
|
|
112
|
-
}
|
|
108
|
+
return ClientService().submit_results(run_type=run_type, run_id=run_id, results=payload)
|
|
@@ -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/db.py
CHANGED
|
@@ -54,7 +54,7 @@ class ScyllaCluster:
|
|
|
54
54
|
return self.cluster.connect(keyspace=self.config["SCYLLA_KEYSPACE_NAME"])
|
|
55
55
|
|
|
56
56
|
@classmethod
|
|
57
|
-
def get(cls, config:
|
|
57
|
+
def get(cls, config: dict = None) -> 'ScyllaCluster':
|
|
58
58
|
if cls.APP_INSTANCE:
|
|
59
59
|
return cls.APP_INSTANCE
|
|
60
60
|
|
argus/backend/models/result.py
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
1
4
|
from cassandra.cqlengine import columns
|
|
2
5
|
from cassandra.cqlengine.models import Model
|
|
3
6
|
from cassandra.cqlengine.usertype import UserType
|
|
4
|
-
from enum import Enum
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class Status(Enum):
|
|
8
|
-
PASS = 0
|
|
9
|
-
WARNING = 1
|
|
10
|
-
ERROR = 2
|
|
11
7
|
|
|
8
|
+
class ValidationRules(UserType):
|
|
9
|
+
valid_from = columns.DateTime()
|
|
10
|
+
best_pct = columns.Double() # max value limit relative to best result in percent unit
|
|
11
|
+
best_abs = columns.Double() # max value limit relative to best result in absolute unit
|
|
12
|
+
fixed_limit = columns.Double() # fixed limit
|
|
12
13
|
|
|
13
14
|
class ColumnMetadata(UserType):
|
|
14
15
|
name = columns.Ascii()
|
|
15
16
|
unit = columns.Text()
|
|
16
17
|
type = columns.Ascii()
|
|
18
|
+
higher_is_better = columns.Boolean() # used for tracking best results, if None - no tracking
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
class ArgusGenericResultMetadata(Model):
|
|
@@ -22,13 +24,68 @@ class ArgusGenericResultMetadata(Model):
|
|
|
22
24
|
name = columns.Text(required=True, primary_key=True)
|
|
23
25
|
description = columns.Text()
|
|
24
26
|
columns_meta = columns.List(value_type=columns.UserDefinedType(ColumnMetadata))
|
|
27
|
+
validation_rules = columns.Map(key_type=columns.Ascii(), value_type=columns.List(columns.UserDefinedType(ValidationRules)))
|
|
25
28
|
rows_meta = columns.List(value_type=columns.Ascii())
|
|
26
29
|
|
|
27
30
|
def __init__(self, **kwargs):
|
|
28
31
|
kwargs["columns_meta"] = [ColumnMetadata(**col) for col in kwargs.pop('columns_meta', [])]
|
|
32
|
+
validation_rules = kwargs.pop('validation_rules', {})
|
|
33
|
+
|
|
34
|
+
if validation_rules:
|
|
35
|
+
for column, rule in validation_rules.items():
|
|
36
|
+
if not isinstance(rule, list):
|
|
37
|
+
rule['valid_from'] = datetime.now(timezone.utc)
|
|
38
|
+
validation_rules[column] = [rule]
|
|
39
|
+
kwargs["validation_rules"] = {k: [ValidationRules(**rules) for rules in v] for k, v in validation_rules.items()}
|
|
29
40
|
super().__init__(**kwargs)
|
|
30
41
|
|
|
31
|
-
def
|
|
42
|
+
def update_validation_rules(self, key: str, new_rule_dict: dict) -> bool:
|
|
43
|
+
"""
|
|
44
|
+
Checks if the most recent ValidationRule for the given key matches the new_rule_dict.
|
|
45
|
+
If not, adds the new rule to the list with the current timestamp.
|
|
46
|
+
|
|
47
|
+
:param key: The key (column name) in the validation_rules map to update.
|
|
48
|
+
:param new_rule_dict: A dictionary containing the new validation rule values.
|
|
49
|
+
:return: True if a new rule was added, False if the existing rule matches.
|
|
50
|
+
"""
|
|
51
|
+
rules_list = self.validation_rules.get(key, [])
|
|
52
|
+
most_recent_rule = None
|
|
53
|
+
|
|
54
|
+
if rules_list:
|
|
55
|
+
most_recent_rule = rules_list[-1]
|
|
56
|
+
|
|
57
|
+
fields_to_compare = [field for field in ValidationRules._fields if field != 'valid_from']
|
|
58
|
+
rules_match = True
|
|
59
|
+
if most_recent_rule:
|
|
60
|
+
for field in fields_to_compare:
|
|
61
|
+
db_value = getattr(most_recent_rule, field)
|
|
62
|
+
new_value = new_rule_dict.get(field)
|
|
63
|
+
if db_value is None and new_value is None:
|
|
64
|
+
continue
|
|
65
|
+
if db_value is None or new_value is None:
|
|
66
|
+
rules_match = False
|
|
67
|
+
break
|
|
68
|
+
if not math.isclose(db_value, new_value, rel_tol=1e-9, abs_tol=0.0):
|
|
69
|
+
rules_match = False
|
|
70
|
+
break
|
|
71
|
+
else:
|
|
72
|
+
rules_match = False
|
|
73
|
+
|
|
74
|
+
if not rules_match:
|
|
75
|
+
new_rule = ValidationRules(
|
|
76
|
+
valid_from=datetime.now(timezone.utc),
|
|
77
|
+
best_pct=new_rule_dict.get('best_pct'),
|
|
78
|
+
best_abs=new_rule_dict.get('best_abs'),
|
|
79
|
+
fixed_limit=new_rule_dict.get('fixed_limit')
|
|
80
|
+
)
|
|
81
|
+
rules_list.append(new_rule)
|
|
82
|
+
self.validation_rules = self.validation_rules or {}
|
|
83
|
+
self.validation_rules.update({key: rules_list})
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False # Existing rule matches
|
|
87
|
+
|
|
88
|
+
def update_if_changed(self, new_data: dict) -> "ArgusGenericResultMetadata":
|
|
32
89
|
"""
|
|
33
90
|
Updates table metadata if changed column/description or new rows were added.
|
|
34
91
|
See that rows can only be added, not removed once was sent.
|
|
@@ -38,18 +95,26 @@ class ArgusGenericResultMetadata(Model):
|
|
|
38
95
|
for field, value in new_data.items():
|
|
39
96
|
if field == "columns_meta":
|
|
40
97
|
value = [ColumnMetadata(**col) for col in value]
|
|
98
|
+
if self.columns_meta != value:
|
|
99
|
+
self.columns_meta = value
|
|
100
|
+
updated = True
|
|
41
101
|
elif field == "rows_meta":
|
|
42
102
|
added_rows = []
|
|
43
103
|
for row in value:
|
|
44
104
|
if row not in self.rows_meta:
|
|
45
105
|
added_rows.append(row)
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
updated = True
|
|
107
|
+
self.rows_meta += added_rows
|
|
108
|
+
elif field == "validation_rules":
|
|
109
|
+
if any([self.update_validation_rules(key, rules) for key, rules in value.items()]):
|
|
110
|
+
updated = True
|
|
111
|
+
elif getattr(self, field) != value:
|
|
48
112
|
setattr(self, field, value)
|
|
49
113
|
updated = True
|
|
50
114
|
|
|
51
115
|
if updated:
|
|
52
116
|
self.save()
|
|
117
|
+
return self
|
|
53
118
|
|
|
54
119
|
class ArgusGenericResultData(Model):
|
|
55
120
|
__table_name__ = "generic_result_data_v1"
|
|
@@ -60,4 +125,14 @@ class ArgusGenericResultData(Model):
|
|
|
60
125
|
row = columns.Ascii(primary_key=True, index=True)
|
|
61
126
|
sut_timestamp = columns.DateTime() # for sorting
|
|
62
127
|
value = columns.Double()
|
|
128
|
+
value_text = columns.Text()
|
|
63
129
|
status = columns.Ascii()
|
|
130
|
+
|
|
131
|
+
class ArgusBestResultData(Model):
|
|
132
|
+
__table_name__ = "generic_result_best_v1"
|
|
133
|
+
test_id = columns.UUID(partition_key=True)
|
|
134
|
+
name = columns.Text(partition_key=True)
|
|
135
|
+
key = columns.Ascii(primary_key=True) # represents pair column:row
|
|
136
|
+
result_date = columns.DateTime(primary_key=True, clustering_order="DESC")
|
|
137
|
+
value = columns.Double()
|
|
138
|
+
run_id = columns.UUID()
|
argus/backend/models/web.py
CHANGED
|
@@ -6,7 +6,7 @@ from cassandra.cqlengine.usertype import UserType
|
|
|
6
6
|
from cassandra.cqlengine import columns
|
|
7
7
|
from cassandra.util import uuid_from_time, unix_time_from_uuid1 # pylint: disable=no-name-in-module
|
|
8
8
|
|
|
9
|
-
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
|
|
9
|
+
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def uuid_now():
|
|
@@ -381,6 +381,7 @@ USED_MODELS: list[Model] = [
|
|
|
381
381
|
ArgusScheduleTest,
|
|
382
382
|
ArgusGenericResultMetadata,
|
|
383
383
|
ArgusGenericResultData,
|
|
384
|
+
ArgusBestResultData,
|
|
384
385
|
]
|
|
385
386
|
|
|
386
387
|
USED_TYPES: list[UserType] = [
|
argus/backend/plugins/loader.py
CHANGED
|
@@ -30,7 +30,7 @@ def plugin_loader() -> dict[str, PluginInfoBase]:
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
AVAILABLE_PLUGINS = plugin_loader()
|
|
33
|
-
|
|
33
|
+
print(AVAILABLE_PLUGINS)
|
|
34
34
|
|
|
35
35
|
def all_plugin_models(include_all=False) -> list[PluginModelBase]:
|
|
36
36
|
return [model for plugin in AVAILABLE_PLUGINS.values() for model in plugin.all_models if issubclass(model, PluginModelBase) or include_all]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
import logging
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from uuid import UUID
|
|
@@ -254,8 +254,15 @@ class SCTTestRun(PluginModelBase):
|
|
|
254
254
|
def sut_timestamp(self) -> float:
|
|
255
255
|
"""converts scylla-server date to timestamp and adds revision in subseconds precision to diffirentiate
|
|
256
256
|
scylla versions from the same day. It's not perfect, but we don't know exact version time."""
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
try:
|
|
258
|
+
scylla_package_upgraded = [package for package in self.packages if package.name == "scylla-server-upgraded"][0]
|
|
259
|
+
except IndexError:
|
|
260
|
+
scylla_package_upgraded = None
|
|
261
|
+
try:
|
|
262
|
+
scylla_package = [package for package in self.packages if package.name == "scylla-server"][0]
|
|
263
|
+
except IndexError:
|
|
264
|
+
raise ValueError("Scylla package not found in packages - cannot determine SUT timestamp")
|
|
265
|
+
return (datetime.strptime(scylla_package.date, '%Y%m%d').replace(tzinfo=timezone.utc).timestamp()
|
|
259
266
|
+ int(scylla_package.revision_id, 16) % 1000000 / 1000000)
|
|
260
267
|
|
|
261
268
|
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
from dataclasses import asdict, is_dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from functools import partial
|
|
1
5
|
from uuid import UUID
|
|
6
|
+
|
|
2
7
|
from argus.backend.db import ScyllaCluster
|
|
3
8
|
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
|
|
4
9
|
from argus.backend.plugins.core import PluginModelBase
|
|
5
10
|
from argus.backend.plugins.loader import AVAILABLE_PLUGINS
|
|
11
|
+
from argus.backend.service.results_service import ResultsService, Cell
|
|
6
12
|
from argus.backend.util.enums import TestStatus
|
|
7
13
|
|
|
8
14
|
|
|
@@ -25,8 +31,9 @@ class ClientService:
|
|
|
25
31
|
def submit_run(self, run_type: str, request_data: dict) -> str:
|
|
26
32
|
model = self.get_model(run_type)
|
|
27
33
|
model.submit_run(request_data=request_data)
|
|
34
|
+
|
|
28
35
|
return "Created"
|
|
29
|
-
|
|
36
|
+
|
|
30
37
|
def get_run(self, run_type: str, run_id: str):
|
|
31
38
|
model = self.get_model(run_type)
|
|
32
39
|
try:
|
|
@@ -79,23 +86,37 @@ class ClientService:
|
|
|
79
86
|
|
|
80
87
|
return "Finalized"
|
|
81
88
|
|
|
82
|
-
def submit_results(self, run_type: str, run_id: str, results: dict) -> str:
|
|
89
|
+
def submit_results(self, run_type: str, run_id: str, results: dict) -> dict[str, str]:
|
|
83
90
|
model = self.get_model(run_type)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
try:
|
|
92
|
+
run = model.load_test_run(UUID(run_id))
|
|
93
|
+
except model.DoesNotExist:
|
|
94
|
+
return {"status": "error", "response": {
|
|
95
|
+
"exception": "DoesNotExist",
|
|
96
|
+
"arguments": [run_id]
|
|
97
|
+
}}
|
|
98
|
+
table_name = results["meta"]["name"]
|
|
99
|
+
results_service = ResultsService()
|
|
100
|
+
cells = [Cell(**cell) for cell in results["results"]]
|
|
101
|
+
table_metadata = results_service.get_table_metadata(test_id=run.test_id, table_name=table_name)
|
|
102
|
+
if table_metadata:
|
|
103
|
+
table_metadata = table_metadata.update_if_changed(results["meta"])
|
|
88
104
|
else:
|
|
89
|
-
ArgusGenericResultMetadata(test_id=run.test_id, **results["meta"])
|
|
105
|
+
table_metadata = ArgusGenericResultMetadata(test_id=run.test_id, **results["meta"])
|
|
106
|
+
table_metadata.save()
|
|
90
107
|
if results.get("sut_timestamp", 0) == 0:
|
|
91
108
|
results["sut_timestamp"] = run.sut_timestamp() # automatic sut_timestamp
|
|
109
|
+
results["sut_timestamp"] = datetime.fromtimestamp(results["sut_timestamp"])
|
|
110
|
+
best_results = results_service.update_best_results(test_id=run.test_id, table_name=table_name, table_metadata=table_metadata,
|
|
111
|
+
cells=cells, run_id=run_id)
|
|
92
112
|
table_name = results["meta"]["name"]
|
|
93
113
|
sut_timestamp = results["sut_timestamp"]
|
|
94
|
-
for cell in
|
|
114
|
+
for cell in cells:
|
|
115
|
+
cell.update_cell_status_based_on_rules(table_metadata, best_results)
|
|
95
116
|
ArgusGenericResultData(test_id=run.test_id,
|
|
96
117
|
run_id=run.id,
|
|
97
118
|
name=table_name,
|
|
98
119
|
sut_timestamp=sut_timestamp,
|
|
99
|
-
**cell
|
|
120
|
+
**asdict(cell)
|
|
100
121
|
).save()
|
|
101
|
-
return "
|
|
122
|
+
return {"status": "ok", "message": "Results submitted"}
|
|
@@ -93,7 +93,8 @@ class NotificationSenderBase:
|
|
|
93
93
|
|
|
94
94
|
class ArgusDBNotificationSaver(NotificationSenderBase):
|
|
95
95
|
CONTENT_TEMPLATES = {
|
|
96
|
-
ArgusNotificationTypes.Mention: lambda p: render_template("notifications/mention.html.j2", **p if p else {})
|
|
96
|
+
ArgusNotificationTypes.Mention: lambda p: render_template("notifications/mention.html.j2", **p if p else {}),
|
|
97
|
+
ArgusNotificationTypes.AssigneeChange: lambda p: render_template("notifications/assigned.html.j2", **p if p else {}),
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
def send_notification(self, receiver: UUID, sender: UUID, notification_type: ArgusNotificationTypes, source_type: ArgusNotificationSourceTypes,
|
|
@@ -117,7 +118,8 @@ class ArgusDBNotificationSaver(NotificationSenderBase):
|
|
|
117
118
|
class EmailNotificationServiceSender(NotificationSenderBase):
|
|
118
119
|
CONTENT_TEMPLATES = {
|
|
119
120
|
ArgusNotificationTypes.Mention: lambda p: render_template(
|
|
120
|
-
"notifications/email_mention.html.j2", **p if p else {})
|
|
121
|
+
"notifications/email_mention.html.j2", **p if p else {}),
|
|
122
|
+
ArgusNotificationTypes.AssigneeChange: lambda p: render_template("notifications/assigned_email.html.j2", **p if p else {}),
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
def __init__(self):
|
|
@@ -1,14 +1,61 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import logging
|
|
3
3
|
import math
|
|
4
|
+
import operator
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from functools import partial
|
|
4
7
|
from typing import List, Dict, Any
|
|
5
8
|
from uuid import UUID
|
|
6
9
|
|
|
10
|
+
from dataclasses import dataclass
|
|
7
11
|
from argus.backend.db import ScyllaCluster
|
|
8
|
-
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData
|
|
12
|
+
from argus.backend.models.result import ArgusGenericResultMetadata, ArgusGenericResultData, ArgusBestResultData
|
|
9
13
|
|
|
10
14
|
LOGGER = logging.getLogger(__name__)
|
|
11
15
|
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class BestResult:
|
|
19
|
+
key: str
|
|
20
|
+
value: float
|
|
21
|
+
result_date: datetime
|
|
22
|
+
run_id: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Cell:
|
|
27
|
+
column: str
|
|
28
|
+
row: str
|
|
29
|
+
status: str
|
|
30
|
+
value: Any | None = None
|
|
31
|
+
value_text: str | None = None
|
|
32
|
+
|
|
33
|
+
def update_cell_status_based_on_rules(self, table_metadata: ArgusGenericResultMetadata, best_results: dict[str, BestResult],
|
|
34
|
+
) -> None:
|
|
35
|
+
column_validation_rules = table_metadata.validation_rules.get(self.column)
|
|
36
|
+
rules = column_validation_rules[-1] if column_validation_rules else {}
|
|
37
|
+
higher_is_better = next((col.higher_is_better for col in table_metadata.columns_meta if col.name == self.column), None)
|
|
38
|
+
if not rules or self.status != "UNSET" or higher_is_better is None:
|
|
39
|
+
return
|
|
40
|
+
is_better = partial(operator.gt, self.value) if higher_is_better else partial(operator.lt, self.value)
|
|
41
|
+
key = f"{self.column}:{self.row}"
|
|
42
|
+
limits = []
|
|
43
|
+
if rules.fixed_limit is not None:
|
|
44
|
+
limits.append(rules.fixed_limit)
|
|
45
|
+
|
|
46
|
+
if best_result := best_results.get(key):
|
|
47
|
+
best_value = best_result.value
|
|
48
|
+
if (best_pct := rules.best_pct) is not None:
|
|
49
|
+
multiplier = 1 - best_pct / 100 if higher_is_better else 1 + best_pct / 100
|
|
50
|
+
limits.append(best_value * multiplier)
|
|
51
|
+
if (best_abs := rules.best_abs) is not None:
|
|
52
|
+
limits.append(best_value - best_abs if higher_is_better else best_value + best_abs)
|
|
53
|
+
if all(is_better(limit) for limit in limits):
|
|
54
|
+
self.status = "PASS"
|
|
55
|
+
else:
|
|
56
|
+
self.status = "ERROR"
|
|
57
|
+
|
|
58
|
+
|
|
12
59
|
default_options = {
|
|
13
60
|
"scales": {
|
|
14
61
|
"y": {
|
|
@@ -67,12 +114,12 @@ colors = [
|
|
|
67
114
|
]
|
|
68
115
|
|
|
69
116
|
|
|
70
|
-
def get_sorted_data_for_column_and_row(data: List[
|
|
117
|
+
def get_sorted_data_for_column_and_row(data: List[ArgusGenericResultData], column: str, row: str) -> List[Dict[str, Any]]:
|
|
71
118
|
return sorted([{"x": entry.sut_timestamp.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
72
119
|
"y": entry.value,
|
|
73
120
|
"id": entry.run_id}
|
|
74
|
-
for entry in data if entry
|
|
75
|
-
key=lambda
|
|
121
|
+
for entry in data if entry.column == column and entry.row == row],
|
|
122
|
+
key=lambda point: point["x"])
|
|
76
123
|
|
|
77
124
|
|
|
78
125
|
def get_min_max_y(datasets: List[Dict[str, Any]]) -> (float, float):
|
|
@@ -105,6 +152,9 @@ def round_datasets_to_min_max(datasets: List[Dict[str, Any]], min_y: float, max_
|
|
|
105
152
|
def create_chartjs(table, data):
|
|
106
153
|
graphs = []
|
|
107
154
|
for column in table.columns_meta:
|
|
155
|
+
if column.type == "TEXT":
|
|
156
|
+
# skip text columns
|
|
157
|
+
continue
|
|
108
158
|
datasets = [
|
|
109
159
|
{"label": row,
|
|
110
160
|
"borderColor": colors[idx % len(colors)],
|
|
@@ -118,7 +168,7 @@ def create_chartjs(table, data):
|
|
|
118
168
|
continue
|
|
119
169
|
options = copy.deepcopy(default_options)
|
|
120
170
|
options["plugins"]["title"]["text"] = f"{table.name} - {column.name}"
|
|
121
|
-
options["scales"]["y"]["title"]["text"] = f"[{column.unit}]"
|
|
171
|
+
options["scales"]["y"]["title"]["text"] = f"[{column.unit}]" if column.unit else ""
|
|
122
172
|
options["scales"]["y"]["min"] = min_y
|
|
123
173
|
options["scales"]["y"]["max"] = max_y
|
|
124
174
|
graphs.append({"options": options, "data":
|
|
@@ -126,15 +176,142 @@ def create_chartjs(table, data):
|
|
|
126
176
|
return graphs
|
|
127
177
|
|
|
128
178
|
|
|
179
|
+
def calculate_graph_ticks(graphs: List[Dict]) -> dict[str, str]:
|
|
180
|
+
min_x, max_x = None, None
|
|
181
|
+
|
|
182
|
+
for graph in graphs:
|
|
183
|
+
for dataset in graph["data"]["datasets"]:
|
|
184
|
+
if not dataset["data"]:
|
|
185
|
+
continue
|
|
186
|
+
first_x = dataset["data"][0]["x"]
|
|
187
|
+
last_x = dataset["data"][-1]["x"]
|
|
188
|
+
if min_x is None or first_x < min_x:
|
|
189
|
+
min_x = first_x
|
|
190
|
+
if max_x is None or last_x > max_x:
|
|
191
|
+
max_x = last_x
|
|
192
|
+
return {"min": min_x[:10], "max": max_x[:10]}
|
|
193
|
+
|
|
194
|
+
|
|
129
195
|
class ResultsService:
|
|
130
196
|
|
|
131
|
-
def __init__(self
|
|
132
|
-
self.
|
|
197
|
+
def __init__(self):
|
|
198
|
+
self.cluster = ScyllaCluster.get()
|
|
133
199
|
|
|
134
|
-
def
|
|
200
|
+
def _get_tables_metadata(self, test_id: UUID) -> list[ArgusGenericResultMetadata]:
|
|
201
|
+
query_fields = ["name", "description", "columns_meta", "rows_meta"]
|
|
202
|
+
raw_query = (f"SELECT {','.join(query_fields)}"
|
|
203
|
+
f" FROM generic_result_metadata_v1 WHERE test_id = ?")
|
|
204
|
+
query = self.cluster.prepare(raw_query)
|
|
205
|
+
tables_meta = self.cluster.session.execute(query=query, parameters=(test_id,))
|
|
206
|
+
return [ArgusGenericResultMetadata(**table) for table in tables_meta]
|
|
207
|
+
|
|
208
|
+
def get_table_metadata(self, test_id: UUID, table_name: str) -> ArgusGenericResultMetadata:
|
|
209
|
+
raw_query = ("SELECT * FROM generic_result_metadata_v1 WHERE test_id = ? AND name = ?")
|
|
210
|
+
query = self.cluster.prepare(raw_query)
|
|
211
|
+
table_meta = self.cluster.session.execute(query=query, parameters=(test_id, table_name))
|
|
212
|
+
return [ArgusGenericResultMetadata(**table) for table in table_meta][0] if table_meta else None
|
|
213
|
+
|
|
214
|
+
def get_run_results(self, test_id: UUID, run_id: UUID) -> list[dict]:
|
|
215
|
+
query_fields = ["column", "row", "value", "value_text", "status"]
|
|
216
|
+
raw_query = (f"SELECT {','.join(query_fields)},WRITETIME(status) as ordering"
|
|
217
|
+
f" FROM generic_result_data_v1 WHERE test_id = ? AND run_id = ? AND name = ?")
|
|
218
|
+
query = self.cluster.prepare(raw_query)
|
|
219
|
+
tables_meta = self._get_tables_metadata(test_id=test_id)
|
|
220
|
+
tables = []
|
|
221
|
+
for table in tables_meta:
|
|
222
|
+
cells = self.cluster.session.execute(query=query, parameters=(test_id, run_id, table.name))
|
|
223
|
+
if not cells:
|
|
224
|
+
continue
|
|
225
|
+
cells = [dict(cell.items()) for cell in cells]
|
|
226
|
+
tables.append({'meta': {
|
|
227
|
+
'name': table.name,
|
|
228
|
+
'description': table.description,
|
|
229
|
+
'columns_meta': table.columns_meta,
|
|
230
|
+
'rows_meta': table.rows_meta,
|
|
231
|
+
},
|
|
232
|
+
'cells': [{k: v for k, v in cell.items() if k in query_fields} for cell in cells],
|
|
233
|
+
'order': min([cell['ordering'] for cell in cells] or [0])})
|
|
234
|
+
return sorted(tables, key=lambda x: x['order'])
|
|
235
|
+
|
|
236
|
+
def get_test_graphs(self, test_id: UUID):
|
|
237
|
+
query_fields = ["run_id", "column", "row", "value", "status", "sut_timestamp"]
|
|
238
|
+
raw_query = (f"SELECT {','.join(query_fields)}"
|
|
239
|
+
f" FROM generic_result_data_v1 WHERE test_id = ? AND name = ? LIMIT 2147483647")
|
|
240
|
+
query = self.cluster.prepare(raw_query)
|
|
241
|
+
tables_meta = self._get_tables_metadata(test_id=test_id)
|
|
135
242
|
graphs = []
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
data = ArgusGenericResultData
|
|
243
|
+
for table in tables_meta:
|
|
244
|
+
data = self.cluster.session.execute(query=query, parameters=(test_id, table.name))
|
|
245
|
+
data = [ArgusGenericResultData(**cell) for cell in data]
|
|
246
|
+
if not data:
|
|
247
|
+
continue
|
|
139
248
|
graphs.extend(create_chartjs(table, data))
|
|
140
|
-
|
|
249
|
+
ticks = calculate_graph_ticks(graphs)
|
|
250
|
+
return graphs, ticks
|
|
251
|
+
|
|
252
|
+
def is_results_exist(self, test_id: UUID):
|
|
253
|
+
"""Verify if results for given test id exist at all."""
|
|
254
|
+
return bool(ArgusGenericResultMetadata.objects(test_id=test_id).only(["name"]).limit(1))
|
|
255
|
+
|
|
256
|
+
def get_best_results(self, test_id: UUID, name: str) -> List[BestResult]:
|
|
257
|
+
query_fields = ["key", "value", "result_date", "run_id"]
|
|
258
|
+
raw_query = (f"SELECT {','.join(query_fields)}"
|
|
259
|
+
f" FROM generic_result_best_v1 WHERE test_id = ? and name = ?")
|
|
260
|
+
query = self.cluster.prepare(raw_query)
|
|
261
|
+
best_results = self.cluster.session.execute(query=query, parameters=(test_id, name))
|
|
262
|
+
return [BestResult(**best) for best in best_results]
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _update_best_value(best_results: dict[str, list[dict]], higher_is_better_map: dict[str, bool | None], cells: list[dict],
|
|
266
|
+
sut_timestamp: float, run_id: str
|
|
267
|
+
) -> dict[str, list[dict]]:
|
|
268
|
+
|
|
269
|
+
for cell in cells:
|
|
270
|
+
if "column" not in cell or "row" not in cell or "value" not in cell:
|
|
271
|
+
continue
|
|
272
|
+
column, row, value = cell["column"], cell["row"], cell["value"]
|
|
273
|
+
key_name = f"{column}_{row}"
|
|
274
|
+
if higher_is_better_map[column] is None:
|
|
275
|
+
# skipping updating best value when higher_is_better is not set (not enabled by user)
|
|
276
|
+
return best_results
|
|
277
|
+
if key_name not in best_results:
|
|
278
|
+
best_results[key_name] = []
|
|
279
|
+
current_best = None
|
|
280
|
+
else:
|
|
281
|
+
current_best = best_results[key_name][-1]
|
|
282
|
+
if current_best["sut_timestamp"].timestamp() > sut_timestamp:
|
|
283
|
+
# skip updating best value when testing older version than current best
|
|
284
|
+
# as would have to update all values between these dates to make cells statuses to be consistent
|
|
285
|
+
return best_results
|
|
286
|
+
|
|
287
|
+
is_better = partial(operator.gt, value) if higher_is_better_map[column] else partial(operator.lt, value)
|
|
288
|
+
if current_best is None or is_better(current_best["value"]):
|
|
289
|
+
best_results[key_name].append({"sut_timestamp": sut_timestamp, "value": value, "run_id": run_id})
|
|
290
|
+
return best_results
|
|
291
|
+
|
|
292
|
+
def update_best_results(self, test_id: UUID, table_name: str, cells: list[Cell],
|
|
293
|
+
table_metadata: ArgusGenericResultMetadata, run_id: str) -> dict[str, BestResult]:
|
|
294
|
+
"""update best results for given test_id and table_name based on cells values - if any value is better than current best"""
|
|
295
|
+
higher_is_better_map = {meta["name"]: meta.higher_is_better for meta in table_metadata.columns_meta}
|
|
296
|
+
best_results = {}
|
|
297
|
+
for best in self.get_best_results(test_id=test_id, name=table_name):
|
|
298
|
+
if best.key not in best_results:
|
|
299
|
+
best_results[best.key] = best
|
|
300
|
+
|
|
301
|
+
for cell in cells:
|
|
302
|
+
if cell.value is None:
|
|
303
|
+
# textual value, skip
|
|
304
|
+
continue
|
|
305
|
+
key = f"{cell.column}:{cell.row}"
|
|
306
|
+
if higher_is_better_map[cell.column] is None:
|
|
307
|
+
# skipping updating best value when higher_is_better is not set (not enabled by user)
|
|
308
|
+
continue
|
|
309
|
+
current_best = best_results.get(key)
|
|
310
|
+
is_better = partial(operator.gt, cell.value) if higher_is_better_map[cell.column] \
|
|
311
|
+
else partial(operator.lt, cell.value)
|
|
312
|
+
if current_best is None or is_better(current_best.value):
|
|
313
|
+
result_date = datetime.now(timezone.utc)
|
|
314
|
+
best_results[key] = BestResult(key=key, value=cell.value, result_date=result_date, run_id=run_id)
|
|
315
|
+
ArgusBestResultData(test_id=test_id, name=table_name, key=key, value=cell.value, result_date=result_date,
|
|
316
|
+
run_id=run_id).save()
|
|
317
|
+
return best_results
|
argus/backend/service/testrun.py
CHANGED
|
@@ -203,6 +203,22 @@ class TestRunService:
|
|
|
203
203
|
group_id=test.group_id,
|
|
204
204
|
test_id=test.id
|
|
205
205
|
)
|
|
206
|
+
if new_assignee_user.id != g.user.id:
|
|
207
|
+
self.notification_manager.send_notification(
|
|
208
|
+
receiver=new_assignee_user.id,
|
|
209
|
+
sender=g.user.id,
|
|
210
|
+
notification_type=ArgusNotificationTypes.AssigneeChange,
|
|
211
|
+
source_type=ArgusNotificationSourceTypes.TestRun,
|
|
212
|
+
source_id=run.id,
|
|
213
|
+
source_message=str(run.test_id),
|
|
214
|
+
content_params={
|
|
215
|
+
"username": g.user.username,
|
|
216
|
+
"run_id": run.id,
|
|
217
|
+
"test_id": test.id,
|
|
218
|
+
"build_id": run.build_id,
|
|
219
|
+
"build_number": get_build_number(run.build_job_url),
|
|
220
|
+
}
|
|
221
|
+
)
|
|
206
222
|
return {
|
|
207
223
|
"test_run_id": run.id,
|
|
208
224
|
"assignee": str(new_assignee_user.id) if new_assignee_user else None
|
|
@@ -307,28 +323,6 @@ class TestRunService:
|
|
|
307
323
|
}
|
|
308
324
|
return response
|
|
309
325
|
|
|
310
|
-
def fetch_results(self, test_id: UUID, run_id: UUID) -> list[dict]:
|
|
311
|
-
cluster = ScyllaCluster.get()
|
|
312
|
-
query_fields = ["column", "row", "value", "status"]
|
|
313
|
-
raw_query = (f"SELECT {','.join(query_fields)},WRITETIME(value) as ordering "
|
|
314
|
-
f"FROM generic_result_data_v1 WHERE test_id = ? AND run_id = ? AND name = ?")
|
|
315
|
-
query = cluster.prepare(raw_query)
|
|
316
|
-
tables_meta = ArgusGenericResultMetadata.filter(test_id=test_id)
|
|
317
|
-
tables = []
|
|
318
|
-
for table in tables_meta:
|
|
319
|
-
cells = cluster.session.execute(query=query, parameters=(test_id, run_id, table.name))
|
|
320
|
-
if not cells:
|
|
321
|
-
continue
|
|
322
|
-
cells = [dict(cell.items()) for cell in cells]
|
|
323
|
-
tables.append({'meta': {
|
|
324
|
-
'name': table.name,
|
|
325
|
-
'description': table.description,
|
|
326
|
-
'columns_meta': table.columns_meta,
|
|
327
|
-
'rows_meta': table.rows_meta,
|
|
328
|
-
},
|
|
329
|
-
'cells': [{k: v for k, v in cell.items() if k in query_fields} for cell in cells],
|
|
330
|
-
'order': min([cell['ordering'] for cell in cells] or [0])})
|
|
331
|
-
return sorted(tables, key=lambda x: x['order'])
|
|
332
326
|
|
|
333
327
|
def submit_github_issue(self, issue_url: str, test_id: UUID, run_id: UUID):
|
|
334
328
|
user_tokens = UserOauthToken.filter(user_id=g.user.id).all()
|
|
File without changes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# BASE URL FOR ARGUS APPLICATION
|
|
2
|
+
BASE_URL: "https://argus.scylladb.com"
|
|
3
|
+
# Main DB Cluster contact points
|
|
4
|
+
SCYLLA_CONTACT_POINTS:
|
|
5
|
+
- 172.18.0.2
|
|
6
|
+
# Username
|
|
7
|
+
SCYLLA_USERNAME: cassandra
|
|
8
|
+
# Password
|
|
9
|
+
SCYLLA_PASSWORD: cassandra
|
|
10
|
+
# Default keyspace (can be created using 'create-keyspace' command with RF set to the number of contact points)
|
|
11
|
+
SCYLLA_KEYSPACE_NAME: test_argus
|
|
12
|
+
# Replication factor used - if set, will override contact_points as amount of nodes for replication
|
|
13
|
+
# SCYLLA_REPLICATION_FACTOR: 3
|
|
14
|
+
LOGIN_METHODS:
|
|
15
|
+
- gh
|
|
16
|
+
# Application log level
|
|
17
|
+
APP_LOG_LEVEL: INFO
|
|
18
|
+
# Secret key used to match session data
|
|
19
|
+
SECRET_KEY: MUSTBEUNIQUE1
|
|
20
|
+
# Client ID of a github oauth application
|
|
21
|
+
GITHUB_CLIENT_ID: not_set
|
|
22
|
+
# Scopes used for Github Application:
|
|
23
|
+
# GITHUB_SCOPES: 'user:email read:user read:org repo'
|
|
24
|
+
# Client secret of a github oauth application
|
|
25
|
+
GITHUB_CLIENT_SECRET: not_set
|
|
26
|
+
# Github personal access token
|
|
27
|
+
GITHUB_ACCESS_TOKEN: unknown
|
|
28
|
+
# List of required organization names (Comment out to disable organization requirement)
|
|
29
|
+
GITHUB_REQUIRED_ORGANIZATIONS:
|
|
30
|
+
# at least one is required for user to successfully authenticate
|
|
31
|
+
BUILD_SYSTEM_FILTERED_PREFIXES:
|
|
32
|
+
- prefixToExclude
|
|
33
|
+
JENKINS_URL: https://jenkins.scylladb.com
|
|
34
|
+
JENKINS_USER: not_set
|
|
35
|
+
JENKINS_API_TOKEN_NAME: not_set
|
|
36
|
+
JENKINS_API_TOKEN: not_set
|
|
37
|
+
JENKINS_MONITORED_RELEASES:
|
|
38
|
+
- not_set
|
|
39
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from _pytest.fixtures import fixture
|
|
6
|
+
|
|
7
|
+
from argus.backend.cli import sync_models
|
|
8
|
+
from argus.backend.db import ScyllaCluster
|
|
9
|
+
from argus.backend.service.client_service import ClientService
|
|
10
|
+
from argus.backend.service.release_manager import ReleaseManagerService
|
|
11
|
+
from argus.backend.util.config import Config
|
|
12
|
+
import logging
|
|
13
|
+
os.environ['CQLENG_ALLOW_SCHEMA_MANAGEMENT'] = '1'
|
|
14
|
+
logging.getLogger('cassandra').setLevel(logging.WARNING)
|
|
15
|
+
logging.getLogger('cassandra.connection').setLevel(logging.WARNING)
|
|
16
|
+
logging.getLogger('cassandra.pool').setLevel(logging.WARNING)
|
|
17
|
+
logging.getLogger('cassandra.cluster').setLevel(logging.WARNING)
|
|
18
|
+
|
|
19
|
+
def truncate_all_tables(session):
|
|
20
|
+
for table in session.cluster.metadata.keyspaces[session.keyspace].tables:
|
|
21
|
+
session.execute(f"TRUNCATE {table}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@fixture(autouse=True, scope='session')
|
|
25
|
+
def argus_db():
|
|
26
|
+
Config.CONFIG_PATHS = [Path(__file__).parent / "argus_web.test.yaml"]
|
|
27
|
+
config = Config.load_yaml_config()
|
|
28
|
+
database = ScyllaCluster.get(config)
|
|
29
|
+
session = database.cluster.connect(keyspace=config["SCYLLA_KEYSPACE_NAME"])
|
|
30
|
+
ScyllaCluster.get_session = lambda: session # monkey patching to escape need for flask app context
|
|
31
|
+
|
|
32
|
+
sync_models()
|
|
33
|
+
truncate_all_tables(database.session)
|
|
34
|
+
yield database
|
|
35
|
+
database.shutdown()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@fixture(autouse=True, scope='session')
|
|
39
|
+
def release_manager_service(argus_db):
|
|
40
|
+
return ReleaseManagerService()
|
|
41
|
+
|
|
42
|
+
@fixture(autouse=True, scope='session')
|
|
43
|
+
def client_service(argus_db):
|
|
44
|
+
return ClientService()
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import uuid
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from typing import Optional, Dict
|
|
5
|
+
|
|
6
|
+
from _pytest.fixtures import fixture
|
|
7
|
+
|
|
8
|
+
from argus.backend.plugins.sct.testrun import SCTTestRunSubmissionRequest
|
|
9
|
+
from argus.client.generic_result import GenericResultTable, ColumnMetadata, ResultType, ValidationRule
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
@fixture(autouse=True, scope='session')
|
|
14
|
+
def release(release_manager_service):
|
|
15
|
+
return release_manager_service.create_release("best_results", "best_results", False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@fixture(autouse=True, scope='session')
|
|
19
|
+
def group(release_manager_service, release):
|
|
20
|
+
return release_manager_service.create_group("br_group", "best_results", build_system_id="best_results", release_id=str(release.id))
|
|
21
|
+
|
|
22
|
+
def get_fake_test_run(
|
|
23
|
+
schema_version: str = "1.0.0",
|
|
24
|
+
run_id: str = str(uuid.uuid4()),
|
|
25
|
+
job_name: str = "default_job_name",
|
|
26
|
+
job_url: str = "http://example.com",
|
|
27
|
+
started_by: str = "default_user",
|
|
28
|
+
commit_id: str = "default_commit_id",
|
|
29
|
+
sct_config: dict | None = None,
|
|
30
|
+
origin_url: str | None = None,
|
|
31
|
+
branch_name: str | None = "main",
|
|
32
|
+
runner_public_ip: str | None = None,
|
|
33
|
+
runner_private_ip: str | None = None
|
|
34
|
+
) -> tuple[str, dict]:
|
|
35
|
+
return "scylla-cluster-tests", asdict(SCTTestRunSubmissionRequest(
|
|
36
|
+
schema_version=schema_version,
|
|
37
|
+
run_id=run_id,
|
|
38
|
+
job_name=job_name,
|
|
39
|
+
job_url=job_url,
|
|
40
|
+
started_by=started_by,
|
|
41
|
+
commit_id=commit_id,
|
|
42
|
+
sct_config=sct_config,
|
|
43
|
+
origin_url=origin_url,
|
|
44
|
+
branch_name=branch_name,
|
|
45
|
+
runner_public_ip=runner_public_ip,
|
|
46
|
+
runner_private_ip=runner_private_ip
|
|
47
|
+
))
|
|
48
|
+
|
|
49
|
+
class SampleTable(GenericResultTable):
|
|
50
|
+
class Meta:
|
|
51
|
+
name = "Test Table Name"
|
|
52
|
+
description = "Test Table Description"
|
|
53
|
+
Columns = [ColumnMetadata(name="float col name", unit="ms", type=ResultType.FLOAT, higher_is_better=False),
|
|
54
|
+
ColumnMetadata(name="int col name", unit="ms", type=ResultType.INTEGER, higher_is_better=False),
|
|
55
|
+
ColumnMetadata(name="duration col name", unit="s", type=ResultType.DURATION, higher_is_better=False),
|
|
56
|
+
ColumnMetadata(name="non tracked col name", unit="", type=ResultType.FLOAT),
|
|
57
|
+
ColumnMetadata(name="text col name", unit="", type=ResultType.TEXT),
|
|
58
|
+
]
|
|
59
|
+
ValidationRules = {"float col name": ValidationRule(best_abs=4),
|
|
60
|
+
"int col name": ValidationRule(best_pct=50, best_abs=5),
|
|
61
|
+
"duration col name": ValidationRule(fixed_limit=590)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def test_argus_tracks_best_result(release_manager_service, client_service, release, group):
|
|
65
|
+
test = release_manager_service.create_test('track_best_result', 'track_best_result', 'track_best_result', 'track_best_result',
|
|
66
|
+
group_id=str(group.id), release_id=str(release.id), plugin_name='sct')
|
|
67
|
+
print(test)
|
|
68
|
+
LOGGER.warning(f"available plugins: {client_service.PLUGINS}")
|
|
69
|
+
client_service.submit_run(*get_fake_test_run())
|
|
70
|
+
assert test
|
argus/client/generic_result.py
CHANGED
|
@@ -7,6 +7,7 @@ class Status(Enum):
|
|
|
7
7
|
PASS = auto()
|
|
8
8
|
WARNING = auto()
|
|
9
9
|
ERROR = auto()
|
|
10
|
+
UNSET = auto()
|
|
10
11
|
|
|
11
12
|
def __str__(self):
|
|
12
13
|
return self.name
|
|
@@ -16,6 +17,7 @@ class ResultType(Enum):
|
|
|
16
17
|
INTEGER = auto()
|
|
17
18
|
FLOAT = auto()
|
|
18
19
|
DURATION = auto()
|
|
20
|
+
TEXT = auto()
|
|
19
21
|
|
|
20
22
|
def __str__(self):
|
|
21
23
|
return self.name
|
|
@@ -26,15 +28,30 @@ class ColumnMetadata:
|
|
|
26
28
|
name: str
|
|
27
29
|
unit: str
|
|
28
30
|
type: ResultType
|
|
31
|
+
higher_is_better: bool = None
|
|
29
32
|
|
|
30
33
|
def as_dict(self) -> dict:
|
|
31
34
|
return {
|
|
32
35
|
"name": self.name,
|
|
33
36
|
"unit": self.unit,
|
|
34
|
-
"type": str(self.type)
|
|
37
|
+
"type": str(self.type),
|
|
38
|
+
"higher_is_better": self.higher_is_better
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
|
|
42
|
+
@dataclass
|
|
43
|
+
class ValidationRule:
|
|
44
|
+
best_pct: float | None = None # max value limit relative to best result in percent unit
|
|
45
|
+
best_abs: float | None = None # max value limit relative to best result in absolute unit
|
|
46
|
+
fixed_limit: float | None = None
|
|
47
|
+
|
|
48
|
+
def as_dict(self) -> dict:
|
|
49
|
+
return {
|
|
50
|
+
"best_pct": self.best_pct,
|
|
51
|
+
"best_abs": self.best_abs,
|
|
52
|
+
"fixed_limit": self.fixed_limit
|
|
53
|
+
}
|
|
54
|
+
|
|
38
55
|
class ResultTableMeta(type):
|
|
39
56
|
def __new__(cls, name, bases, dct):
|
|
40
57
|
cls_instance = super().__new__(cls, name, bases, dct)
|
|
@@ -44,8 +61,16 @@ class ResultTableMeta(type):
|
|
|
44
61
|
cls_instance.name = meta.name
|
|
45
62
|
cls_instance.description = meta.description
|
|
46
63
|
cls_instance.columns = meta.Columns
|
|
47
|
-
cls_instance.
|
|
64
|
+
cls_instance.column_types = {column.name: column.type for column in cls_instance.columns}
|
|
48
65
|
cls_instance.rows = []
|
|
66
|
+
for col_name, rule in meta.ValidationRules.items():
|
|
67
|
+
if col_name not in cls_instance.column_types:
|
|
68
|
+
raise ValueError(f"ValidationRule column {col_name} not found in the table")
|
|
69
|
+
if cls_instance.column_types[col_name] == ResultType.TEXT:
|
|
70
|
+
raise ValueError(f"Validation rules don't apply to TEXT columns")
|
|
71
|
+
if not isinstance(rule, ValidationRule):
|
|
72
|
+
raise ValueError(f"Validation rule for column {col_name} is not of type ValidationRule")
|
|
73
|
+
cls_instance.validation_rules = meta.ValidationRules
|
|
49
74
|
return cls_instance
|
|
50
75
|
|
|
51
76
|
|
|
@@ -57,12 +82,13 @@ class Cell:
|
|
|
57
82
|
status: Status
|
|
58
83
|
|
|
59
84
|
def as_dict(self) -> dict:
|
|
60
|
-
|
|
85
|
+
cell = {"value_text": self.value} if isinstance(self.value, str) else {"value": self.value}
|
|
86
|
+
cell.update({
|
|
61
87
|
"column": self.column,
|
|
62
88
|
"row": self.row,
|
|
63
|
-
"value": self.value,
|
|
64
89
|
"status": str(self.status)
|
|
65
|
-
}
|
|
90
|
+
})
|
|
91
|
+
return cell
|
|
66
92
|
|
|
67
93
|
|
|
68
94
|
@dataclass
|
|
@@ -84,7 +110,8 @@ class GenericResultTable(metaclass=ResultTableMeta):
|
|
|
84
110
|
"name": self.name,
|
|
85
111
|
"description": self.description,
|
|
86
112
|
"columns_meta": [column.as_dict() for column in self.columns],
|
|
87
|
-
"rows_meta": rows
|
|
113
|
+
"rows_meta": rows,
|
|
114
|
+
"validation_rules": {k: v.as_dict() for k, v in self.validation_rules.items()}
|
|
88
115
|
}
|
|
89
116
|
return {
|
|
90
117
|
"meta": meta_info,
|
|
@@ -94,6 +121,8 @@ class GenericResultTable(metaclass=ResultTableMeta):
|
|
|
94
121
|
}
|
|
95
122
|
|
|
96
123
|
def add_result(self, column: str, row: str, value: Union[int, float, str], status: Status):
|
|
97
|
-
if column not in self.
|
|
124
|
+
if column not in self.column_types:
|
|
98
125
|
raise ValueError(f"Column {column} not found in the table")
|
|
126
|
+
if isinstance(value, str) and self.column_types[column] != ResultType.TEXT:
|
|
127
|
+
raise ValueError(f"Column {column} is not of type TEXT")
|
|
99
128
|
self.results.append(Cell(column=column, row=row, value=value, status=status))
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
from dataclasses import dataclass, field, asdict
|
|
2
|
+
from typing import Dict, List, Tuple, Union, Type
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
from enum import Enum
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Status(Enum):
|
|
10
|
+
PASS = 1
|
|
11
|
+
WARNING = 2
|
|
12
|
+
ERROR = 3
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ResultType(Enum):
|
|
16
|
+
INTEGER = int
|
|
17
|
+
FLOAT = float
|
|
18
|
+
TEXT = str
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class ColumnMetadata:
|
|
23
|
+
id: int
|
|
24
|
+
unit: str
|
|
25
|
+
type: ResultType
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class Result:
|
|
30
|
+
value: Union[int, float, str]
|
|
31
|
+
status: Status
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ResultTableMeta(type):
|
|
35
|
+
def __new__(cls, name, bases, dct):
|
|
36
|
+
cls_instance = super().__new__(cls, name, bases, dct)
|
|
37
|
+
meta = dct.get('Meta')
|
|
38
|
+
|
|
39
|
+
if meta:
|
|
40
|
+
cls_instance.table_name = meta.table_name
|
|
41
|
+
cls_instance.columns_map = {col_name: ColumnMetadata(id=col_id, unit=unit, type=result_type)
|
|
42
|
+
for col_name, (col_id, unit, result_type) in meta.Columns.items()}
|
|
43
|
+
cls_instance.rows_map = {row_name: row_id
|
|
44
|
+
for row_name, row_id in meta.Rows.items()}
|
|
45
|
+
return cls_instance
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Row:
|
|
49
|
+
def __init__(self, columns_map: Dict[str, ColumnMetadata]):
|
|
50
|
+
self.columns_map = columns_map
|
|
51
|
+
self.data: Dict[str, Result] = {}
|
|
52
|
+
|
|
53
|
+
def __getitem__(self, column_name: str) -> Result:
|
|
54
|
+
return self.data[column_name]
|
|
55
|
+
|
|
56
|
+
def __setitem__(self, column_name: str, result: Result):
|
|
57
|
+
if column_name not in self.columns_map:
|
|
58
|
+
raise ValueError(f"Column name '{column_name}' not found in columns_map.")
|
|
59
|
+
column_metadata = self.columns_map[column_name]
|
|
60
|
+
if not isinstance(result.value, column_metadata.type.value):
|
|
61
|
+
raise ValueError(f"Value {result.value} for column '{column_name}' is not of type {column_metadata.type.name}")
|
|
62
|
+
self.data[column_name] = result
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class BaseResultTable(metaclass=ResultTableMeta):
|
|
67
|
+
results: Dict[str, Row] = field(default_factory=dict)
|
|
68
|
+
|
|
69
|
+
def as_dict(self) -> dict:
|
|
70
|
+
results_list = []
|
|
71
|
+
for row_name, row in self.results.items():
|
|
72
|
+
row_id = self.rows_map.get(row_name)
|
|
73
|
+
for column_name, result in row.data.items():
|
|
74
|
+
column_metadata = self.columns_map.get(column_name)
|
|
75
|
+
results_list.append({
|
|
76
|
+
"column_id": column_metadata.id,
|
|
77
|
+
"row_id": row_id,
|
|
78
|
+
"result": result.value,
|
|
79
|
+
"status": result.status.value
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
meta_info = {
|
|
83
|
+
"table_name": self.table_name,
|
|
84
|
+
"columns": {name: {"id": meta.id, "unit": meta.unit, "type": meta.type.name} for name, meta in self.columns_map.items()},
|
|
85
|
+
"rows": self.rows_map
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
"meta": meta_info,
|
|
89
|
+
"results": results_list
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def to_json(self) -> str:
|
|
93
|
+
dict_result = self.as_dict()
|
|
94
|
+
return json.dumps(dict_result, default=str)
|
|
95
|
+
|
|
96
|
+
def __getitem__(self, row_name: str) -> Row:
|
|
97
|
+
if row_name not in self.rows_map:
|
|
98
|
+
raise ValueError(f"Row name '{row_name}' not found in rows_map.")
|
|
99
|
+
if row_name not in self.results:
|
|
100
|
+
self.results[row_name] = Row(self.columns_map)
|
|
101
|
+
return self.results[row_name]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Example of a specific result table with its own metadata
|
|
105
|
+
class LatencyResultTable(BaseResultTable):
|
|
106
|
+
class Meta:
|
|
107
|
+
table_name = "latency_percentile_write"
|
|
108
|
+
Columns = {
|
|
109
|
+
"latency": (1, "ms", ResultType.FLOAT),
|
|
110
|
+
"op_rate": (2, "ops", ResultType.INTEGER)
|
|
111
|
+
}
|
|
112
|
+
Rows = {
|
|
113
|
+
"mean": 1,
|
|
114
|
+
"p99": 2
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Client code example
|
|
119
|
+
if __name__ == "__main__":
|
|
120
|
+
class LatencyResultTable(BaseResultTable):
|
|
121
|
+
class Meta:
|
|
122
|
+
table_name = "latency_percentile_write"
|
|
123
|
+
Columns = {
|
|
124
|
+
"latency": (1, "ms", ResultType.FLOAT),
|
|
125
|
+
"op_rate": (2, "ops", ResultType.INTEGER)
|
|
126
|
+
}
|
|
127
|
+
Rows = {
|
|
128
|
+
"mean": 1,
|
|
129
|
+
"p99": 2
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
result_table = LatencyResultTable()
|
|
133
|
+
|
|
134
|
+
result_table["mean"]["latency"] = Result(value=1.1, status=Status.WARNING)
|
|
135
|
+
result_table["mean"]["op_rate"] = Result(value=59988, status=Status.ERROR)
|
|
136
|
+
result_table["p99"]["latency"] = Result(value=2.7, status=Status.PASS)
|
|
137
|
+
result_table["p99"]["op_rate"] = Result(value=59988, status=Status.WARNING)
|
|
138
|
+
|
|
139
|
+
from argus.client.sct.client import ArgusSCTClient
|
|
140
|
+
|
|
141
|
+
run_id = UUID("24e09748-bba4-47fd-a615-bf7ea2c425eb")
|
|
142
|
+
client = ArgusSCTClient(run_id, auth_token="UO+2GXL9XqSgcVJijWk5WnbPXPit5ot5nfkLAHAr7SaqROfSCWycabpp/wxyY8+I", base_url="http://localhost:5000")
|
|
143
|
+
client.submit_results(result_table)
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
argus/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
argus/backend/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
argus/backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
argus/backend/cli.py,sha256=
|
|
4
|
+
argus/backend/cli.py,sha256=ULa9KVv7RR3DVHKlYvsSdIfbX5s-VuiPuhWWRfPkdGU,1383
|
|
5
5
|
argus/backend/controller/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
argus/backend/controller/admin.py,sha256=2z29RX7ZQO_VTklSKH9RrEj-Ag2SsvyOaIzWDKr0ahQ,575
|
|
7
7
|
argus/backend/controller/admin_api.py,sha256=lj5g6rdoKN9X13H9hXmKYx_-9tftt6HSftiNFLCr_kY,8567
|
|
8
|
-
argus/backend/controller/api.py,sha256=
|
|
8
|
+
argus/backend/controller/api.py,sha256=722KPfrclfxHHf7eUai-ZRv9L-3ehs6FGHr5Qs6AsIU,14827
|
|
9
9
|
argus/backend/controller/auth.py,sha256=rGKgqqjfiZOnoOEpthg5JTd1BuotcEk0S95z_USrgN4,2291
|
|
10
|
-
argus/backend/controller/client_api.py,sha256=
|
|
10
|
+
argus/backend/controller/client_api.py,sha256=ZUVDnaO9b6OesEVvYlqegnkx50vnwCAq8aCNCFPk3JA,3574
|
|
11
11
|
argus/backend/controller/main.py,sha256=EXrwvGq2TyebT8a54Ojkxq-o5_QOhm0w51M53G0h_n0,9060
|
|
12
12
|
argus/backend/controller/notification_api.py,sha256=wz7V4nE6Mxclpq78P8gNnCyeQ7xA9BBJjZ-dPhLLd2I,1964
|
|
13
13
|
argus/backend/controller/notifications.py,sha256=zMSJln72BGU6Q_nQvJesMnuvJ57Ucbov4M2ZI-37Bxo,290
|
|
14
14
|
argus/backend/controller/team.py,sha256=G6LdIBaYgfG0Qr4RhNQ53MZVdh4wcuotsIIpFwhTJ3w,3101
|
|
15
15
|
argus/backend/controller/team_ui.py,sha256=B7N1_Kzl6Rac8BV3FbKj55pGAS_dht47rYhAi94PC8A,589
|
|
16
|
-
argus/backend/controller/testrun_api.py,sha256=
|
|
16
|
+
argus/backend/controller/testrun_api.py,sha256=MCcVmbfNuyODCVbxF8TfcvVu9sMT0xO1UzS4VgumQiM,12638
|
|
17
17
|
argus/backend/controller/view_api.py,sha256=rI7LwcS7keK37nYx76D9StFV_rLHcNkHan8OhFgBrhM,4106
|
|
18
|
-
argus/backend/db.py,sha256=
|
|
18
|
+
argus/backend/db.py,sha256=IgeGjZKTHSoyGrd5f2A7uYn6Pm6DU2-ZIESHmfJwkoE,4099
|
|
19
19
|
argus/backend/error_handlers.py,sha256=IEjz7Vzfldv1PTOeHrpRWmRsgBrHtAW0PXHUJZDovAE,480
|
|
20
20
|
argus/backend/events/event_processors.py,sha256=bsmBayiXvlGn3aqiT2z9WgwnVBRtn2cRqkgn4pLodck,1291
|
|
21
21
|
argus/backend/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
|
-
argus/backend/models/result.py,sha256=
|
|
23
|
-
argus/backend/models/web.py,sha256=
|
|
22
|
+
argus/backend/models/result.py,sha256=DOgxHwerJIyMsqsbDdMYSojkUPiWsu6mHCq9B8JpaBA,5899
|
|
23
|
+
argus/backend/models/web.py,sha256=eJybumxShBrFIdi0H0zHcEuAusfpZKpDrZ4Bxl_JyUY,13141
|
|
24
24
|
argus/backend/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
argus/backend/plugins/core.py,sha256=UsrK8oWyhpDSLEcnqAQsEqYVUIX0eCQB862Em7GMQLo,8690
|
|
26
26
|
argus/backend/plugins/driver_matrix_tests/controller.py,sha256=GdPpProzsVXQw8A4h2IS8inUPdr_Q4IN93i6ocOThS8,2213
|
|
@@ -32,12 +32,12 @@ argus/backend/plugins/driver_matrix_tests/udt.py,sha256=WRtnJU1dZcLXZJQgfU0mgjNz
|
|
|
32
32
|
argus/backend/plugins/generic/model.py,sha256=QLVO7QhGr38Hz0VO-BlDYF7LhRX7Pl049vw4W_VMT8o,3302
|
|
33
33
|
argus/backend/plugins/generic/plugin.py,sha256=5URbQVUCizrk-KZqb6I0P_8nLUekjYh-Js7ZLKVoBAA,407
|
|
34
34
|
argus/backend/plugins/generic/types.py,sha256=jlZUcQ7r153ziyl3ZJmix7AzL2G1aX9N_z-4Kw9trWc,267
|
|
35
|
-
argus/backend/plugins/loader.py,sha256=
|
|
35
|
+
argus/backend/plugins/loader.py,sha256=L3OUNt0e2RrxGD5dIl0PonEAKZj0f9gXcpk7_ifjVuc,1414
|
|
36
36
|
argus/backend/plugins/sct/controller.py,sha256=NF11JLoUJ13whghlxRrVex9rLMgFtlkczUAAKAM9vYg,5738
|
|
37
37
|
argus/backend/plugins/sct/plugin.py,sha256=_sOMcXLoFfeG9jwj_t48C4IFvY87juK8ApR6tfSw6q4,1007
|
|
38
38
|
argus/backend/plugins/sct/resource_setup.py,sha256=hwfAOu-oKOH42tjtzJhiqwq_MtUE9_HevoFyql8JKqY,10120
|
|
39
39
|
argus/backend/plugins/sct/service.py,sha256=ygAL85BkyyovJ1xHktlCQJdJS8CrerJZ_Tbr3EXqsg4,22021
|
|
40
|
-
argus/backend/plugins/sct/testrun.py,sha256=
|
|
40
|
+
argus/backend/plugins/sct/testrun.py,sha256=O7LanXF-fHiHfUAqsr7ALhTmcpVfjAYQODQTdB-u3Ig,10656
|
|
41
41
|
argus/backend/plugins/sct/types.py,sha256=Gw1y4iqYguqNqTh_GopLDFho8vuGaOGuK7fjaHYhAOQ,1326
|
|
42
42
|
argus/backend/plugins/sct/udt.py,sha256=V_x8_yw8rV7Q_QRBYayqtTNsPdZvjzOxWpRhXP1XAzs,3119
|
|
43
43
|
argus/backend/plugins/sirenada/model.py,sha256=KVnI75BacuBryc5lR_Aai-mEOs7CB9xxhb7J-YRU3bc,4705
|
|
@@ -46,18 +46,23 @@ argus/backend/plugins/sirenada/types.py,sha256=Gm3XMK9YJoozVaeM9XE7n8iRxA6PKBrS2
|
|
|
46
46
|
argus/backend/service/admin.py,sha256=_VnWl3CkZBOAie_pPbd9sbXZUpBf2SApyNoFZLfB_QI,637
|
|
47
47
|
argus/backend/service/argus_service.py,sha256=YF6El9CyIelePDrCydVn4K82sd7CzCoZNmcvn2ZeR9I,29266
|
|
48
48
|
argus/backend/service/build_system_monitor.py,sha256=QB7RfMMuA2VJ4oUAOAqLxOwxqaQE52_4ZhsASVcoXkU,8296
|
|
49
|
-
argus/backend/service/client_service.py,sha256=
|
|
49
|
+
argus/backend/service/client_service.py,sha256=XAAZ8VOVg2ppwcWVh8asGvYdlMuqFWjkcF-z-DCKD9c,4754
|
|
50
50
|
argus/backend/service/event_service.py,sha256=iYeqxN2QCYTjYB1WPPv4BEFLXG0Oz3TvskkaK4v9pVY,654
|
|
51
51
|
argus/backend/service/jenkins_service.py,sha256=njomagkliIWKisR9FmhKKqZ8y9NijyJ3hUQe23gl2U4,9878
|
|
52
|
-
argus/backend/service/notification_manager.py,sha256=
|
|
52
|
+
argus/backend/service/notification_manager.py,sha256=hOeFTZ8HhcFa7Ifo9aS-DUsivo5MoCTHyjmUgvH2fyY,7271
|
|
53
53
|
argus/backend/service/release_manager.py,sha256=d1J6llBb4aKgFPrsPTPYpV9NnGx772jeORZjs-ojYGE,7771
|
|
54
|
-
argus/backend/service/results_service.py,sha256=
|
|
54
|
+
argus/backend/service/results_service.py,sha256=WeVeJbOOA1KJCTw3vIUcbWE4YqldwwnTT-mYJMnUqvA,13415
|
|
55
55
|
argus/backend/service/stats.py,sha256=-V94A8EUlQBvwG53oJTL4U1EzR4vciEF7Niu-efTL6Y,22713
|
|
56
56
|
argus/backend/service/team_manager_service.py,sha256=zY5dvy3ffvQbJuXBvlWKE5dS5LQ3ss6tkFE-cwFZsdw,3010
|
|
57
|
-
argus/backend/service/testrun.py,sha256=
|
|
57
|
+
argus/backend/service/testrun.py,sha256=L9JC0D9J8D5QkuT_HxFBK_Z31j1rdiT1cRo7s4zjhqo,22251
|
|
58
58
|
argus/backend/service/user.py,sha256=DC8fII7mElWGB-pMGyn4uzaJHIbmxzh_ZWf2POmlBkU,10936
|
|
59
59
|
argus/backend/service/views.py,sha256=gUzwQv3fasGh0hRvivCr64XooQhG3c1V1KcxgMjC2qM,11292
|
|
60
60
|
argus/backend/template_filters.py,sha256=04PHl0DiN4PBHQ82HMAmTfww09fGMXcYy-I5BU_b1s4,682
|
|
61
|
+
argus/backend/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
|
+
argus/backend/tests/argus_web.test.yaml,sha256=_ompiLY3zXuGjuMenIR0UtJmkTW3RjFWejFS05o850I,1325
|
|
63
|
+
argus/backend/tests/conftest.py,sha256=rg1KVQhO0wEs98HYV4kwtQxODUXq-S1MUy9_53v8qbI,1561
|
|
64
|
+
argus/backend/tests/results_service/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
|
+
argus/backend/tests/results_service/test_best_results.py,sha256=JE1sOecg_TPmdcpteSUMSFx4VFwXUvyJ3736R8l7IiU,3095
|
|
61
66
|
argus/backend/util/common.py,sha256=vLMit9ZBBN8S4-dw32LIhjtaEOX_5hwWneHILS_SNBg,1723
|
|
62
67
|
argus/backend/util/config.py,sha256=1HpHm8Du6yz61gwAE1vR6uwuHCStaSerirbEhBLnDws,927
|
|
63
68
|
argus/backend/util/encoders.py,sha256=5AfJbs2V3gOOg5LtFLZAtBqlnSdX8HHITT7r9Wu-law,1129
|
|
@@ -71,7 +76,8 @@ argus/client/driver_matrix_tests/cli.py,sha256=PIK4IyA4qku7jCnJ8A0i59DeVl1jvMWYu
|
|
|
71
76
|
argus/client/driver_matrix_tests/client.py,sha256=UPryBku2rg6IV2wKKDkclXHnH3r6EYwWdds65wLC-KU,2748
|
|
72
77
|
argus/client/generic/cli.py,sha256=IJkgEZ5VOAeqp5SlLM13Y5m8e34Cqnyz8WkfeKoN7so,2208
|
|
73
78
|
argus/client/generic/client.py,sha256=l4PDjDy65Mm2OI9ZLSnyd8_2i4Ei1Pp9yRt3bRX8s2Y,1114
|
|
74
|
-
argus/client/generic_result.py,sha256=
|
|
79
|
+
argus/client/generic_result.py,sha256=Fyo-ooFoO-w64BbXAlPgg0XOuuozTjv-51rAdsxecno,4197
|
|
80
|
+
argus/client/generic_result_old.py,sha256=Oi15Gu8WbXK_WruF0IU-Fokr-I1k8mzg1MpHbmpt50M,4662
|
|
75
81
|
argus/client/sct/client.py,sha256=DtRA0Ra3ycUcedDYfZZW1jER0nc8vdYHaY6DT0te4x0,11341
|
|
76
82
|
argus/client/sct/types.py,sha256=VLgVe7qPmJtCLqtPnuX8N8kMKZq-iY3SKz68nvU6nJ4,371
|
|
77
83
|
argus/client/sirenada/client.py,sha256=ilcyLXJb-0gKbmb9WSPr-Yvldh73joGBhRDoilQoSJ4,6220
|
|
@@ -83,8 +89,8 @@ argus/db/db_types.py,sha256=iLbmrUaDzrBw0kDCnvW0FSZ9-kNc3uQY-fsbIPymV4E,3612
|
|
|
83
89
|
argus/db/interface.py,sha256=HroyA1Yijz5cXLdYbxorHCEu0GH9VeMMqB36IHTlcew,17146
|
|
84
90
|
argus/db/testrun.py,sha256=0YG7FIH5FLQeNlYULxC6rhhyru2rziSMe3qKtYzTBnc,26014
|
|
85
91
|
argus/db/utils.py,sha256=YAWsuLjUScSgKgdaL5aF4Sgr13gqH29Mb5cLctX4V_w,337
|
|
86
|
-
argus_alm-0.12.
|
|
87
|
-
argus_alm-0.12.
|
|
88
|
-
argus_alm-0.12.
|
|
89
|
-
argus_alm-0.12.
|
|
90
|
-
argus_alm-0.12.
|
|
92
|
+
argus_alm-0.12.9.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
93
|
+
argus_alm-0.12.9.dist-info/METADATA,sha256=i8u6MV6uYgTu74q8FWwy7eYWDaMmrMGrh0VNRF01-k8,3508
|
|
94
|
+
argus_alm-0.12.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
95
|
+
argus_alm-0.12.9.dist-info/entry_points.txt,sha256=pcYW8nxZuDaymxE8tn86K0dq8eEodUdiS0sSvwEQ_zU,137
|
|
96
|
+
argus_alm-0.12.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|