qase-python-commons 3.1.9__py3-none-any.whl → 4.1.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.
- qase/__init__.py +3 -0
- qase/commons/client/api_v1_client.py +269 -175
- qase/commons/client/api_v2_client.py +163 -26
- qase/commons/client/base_api_client.py +23 -6
- qase/commons/config.py +162 -23
- qase/commons/logger.py +82 -13
- qase/commons/models/__init__.py +0 -2
- qase/commons/models/attachment.py +11 -8
- qase/commons/models/basemodel.py +12 -3
- qase/commons/models/config/framework.py +17 -0
- qase/commons/models/config/qaseconfig.py +34 -0
- qase/commons/models/config/run.py +19 -0
- qase/commons/models/config/testops.py +45 -3
- qase/commons/models/external_link.py +41 -0
- qase/commons/models/relation.py +16 -6
- qase/commons/models/result.py +16 -31
- qase/commons/models/run.py +17 -2
- qase/commons/models/runtime.py +9 -0
- qase/commons/models/step.py +45 -12
- qase/commons/profilers/__init__.py +4 -3
- qase/commons/profilers/db.py +965 -5
- qase/commons/reporters/core.py +60 -10
- qase/commons/reporters/report.py +11 -6
- qase/commons/reporters/testops.py +56 -27
- qase/commons/status_mapping/__init__.py +12 -0
- qase/commons/status_mapping/status_mapping.py +237 -0
- qase/commons/util/__init__.py +9 -0
- qase/commons/util/host_data.py +147 -0
- qase/commons/utils.py +95 -0
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/METADATA +16 -11
- qase_python_commons-4.1.9.dist-info/RECORD +45 -0
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/WHEEL +1 -1
- qase/commons/models/suite.py +0 -13
- qase_python_commons-3.1.9.dist-info/RECORD +0 -40
- {qase_python_commons-3.1.9.dist-info → qase_python_commons-4.1.9.dist-info}/top_level.txt +0 -0
qase/commons/logger.py
CHANGED
|
@@ -1,32 +1,101 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import datetime
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Optional
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
|
|
7
|
+
class LoggingOptions:
|
|
8
|
+
def __init__(self, console: bool = True, file: bool = False):
|
|
9
|
+
self.console = console
|
|
10
|
+
self.file = file
|
|
4
11
|
|
|
5
12
|
|
|
6
13
|
class Logger:
|
|
7
|
-
|
|
14
|
+
_log_file = None
|
|
15
|
+
|
|
16
|
+
def __init__(self, debug: bool = False, prefix: str = '', dir: str = os.path.join('.', 'logs'),
|
|
17
|
+
logging_options: Optional[LoggingOptions] = None) -> None:
|
|
8
18
|
self.debug = debug
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
self.prefix = prefix
|
|
20
|
+
self.dir = dir
|
|
21
|
+
|
|
22
|
+
# Initialize logging options
|
|
23
|
+
if logging_options is None:
|
|
24
|
+
# Default behavior: console always enabled, file enabled only in debug mode
|
|
25
|
+
self.logging_options = LoggingOptions(
|
|
26
|
+
console=True,
|
|
27
|
+
file=debug
|
|
28
|
+
)
|
|
29
|
+
else:
|
|
30
|
+
self.logging_options = logging_options
|
|
31
|
+
|
|
32
|
+
# Override with environment variables if set
|
|
33
|
+
self._load_env_logging_options()
|
|
34
|
+
|
|
35
|
+
# Setup file logging if enabled
|
|
36
|
+
if self.logging_options.file:
|
|
37
|
+
if Logger._log_file is None:
|
|
38
|
+
timestamp = self._get_timestamp()
|
|
39
|
+
filename = f'{prefix}_{timestamp}.log'
|
|
40
|
+
if not os.path.exists(dir):
|
|
41
|
+
os.makedirs(dir)
|
|
42
|
+
Logger._log_file = os.path.join(dir, filename)
|
|
43
|
+
|
|
44
|
+
with open(Logger._log_file, 'a', encoding='utf-8'):
|
|
15
45
|
pass
|
|
16
46
|
|
|
47
|
+
self.lock = threading.Lock()
|
|
48
|
+
|
|
49
|
+
def _load_env_logging_options(self):
|
|
50
|
+
"""Load logging options from environment variables"""
|
|
51
|
+
# QASE_LOGGING_CONSOLE
|
|
52
|
+
console_env = os.environ.get('QASE_LOGGING_CONSOLE')
|
|
53
|
+
if console_env is not None:
|
|
54
|
+
self.logging_options.console = console_env.lower() in ('true', '1', 'yes', 'on')
|
|
55
|
+
|
|
56
|
+
# QASE_LOGGING_FILE
|
|
57
|
+
file_env = os.environ.get('QASE_LOGGING_FILE')
|
|
58
|
+
if file_env is not None:
|
|
59
|
+
self.logging_options.file = file_env.lower() in ('true', '1', 'yes', 'on')
|
|
60
|
+
|
|
61
|
+
# Legacy QASE_DEBUG support
|
|
62
|
+
debug_env = os.environ.get('QASE_DEBUG')
|
|
63
|
+
if debug_env is not None and debug_env.lower() in ('true', '1', 'yes', 'on'):
|
|
64
|
+
# When debug is enabled via env, enable file logging if not explicitly disabled
|
|
65
|
+
if not hasattr(self.logging_options, 'file') or self.logging_options.file is None:
|
|
66
|
+
self.logging_options.file = True
|
|
67
|
+
|
|
17
68
|
def log(self, message: str, level: str = 'info'):
|
|
18
69
|
time_str = self._get_timestamp("%H:%M:%S")
|
|
19
70
|
log = f"[Qase][{time_str}][{level}] {message}\n"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
71
|
+
|
|
72
|
+
# Console output
|
|
73
|
+
if self.logging_options.console:
|
|
74
|
+
try:
|
|
75
|
+
print(log, end='')
|
|
76
|
+
except (OSError, IOError):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
# File output
|
|
80
|
+
if self.logging_options.file:
|
|
81
|
+
with self.lock:
|
|
82
|
+
with open(Logger._log_file, 'a', encoding='utf-8') as f:
|
|
83
|
+
f.write(log)
|
|
24
84
|
|
|
25
85
|
def log_debug(self, message: str):
|
|
26
86
|
if self.debug:
|
|
27
87
|
self.log(message, 'debug')
|
|
28
88
|
|
|
89
|
+
def log_error(self, message: str):
|
|
90
|
+
self.log(message, 'error')
|
|
91
|
+
|
|
92
|
+
def log_warning(self, message: str):
|
|
93
|
+
self.log(message, 'warning')
|
|
94
|
+
|
|
95
|
+
def log_info(self, message: str):
|
|
96
|
+
self.log(message, 'info')
|
|
97
|
+
|
|
29
98
|
@staticmethod
|
|
30
|
-
def _get_timestamp(fmt: str = "%Y%m%
|
|
99
|
+
def _get_timestamp(fmt: str = "%Y%m%d"):
|
|
31
100
|
now = datetime.datetime.now()
|
|
32
101
|
return now.strftime(fmt)
|
qase/commons/models/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from .result import Result, Field
|
|
2
2
|
from .run import Run
|
|
3
3
|
from .attachment import Attachment
|
|
4
|
-
from .suite import Suite
|
|
5
4
|
from .relation import Relation
|
|
6
5
|
from .step import Step
|
|
7
6
|
from .runtime import Runtime
|
|
@@ -10,7 +9,6 @@ __all__ = [
|
|
|
10
9
|
Result,
|
|
11
10
|
Run,
|
|
12
11
|
Attachment,
|
|
13
|
-
Suite,
|
|
14
12
|
Relation,
|
|
15
13
|
Step,
|
|
16
14
|
Runtime,
|
|
@@ -29,16 +29,19 @@ class Attachment(BaseModel):
|
|
|
29
29
|
def get_id(self) -> str:
|
|
30
30
|
return self.id
|
|
31
31
|
|
|
32
|
-
def get_for_upload(self) ->
|
|
32
|
+
def get_for_upload(self) -> tuple:
|
|
33
|
+
"""Returns attachment data in format expected by new API client: (filename, filedata)"""
|
|
34
|
+
|
|
33
35
|
if self.file_path:
|
|
34
36
|
with open(self.file_path, "rb") as fc:
|
|
35
|
-
|
|
37
|
+
filedata = fc.read()
|
|
36
38
|
else:
|
|
37
39
|
if isinstance(self.content, str):
|
|
38
|
-
|
|
40
|
+
filedata = bytes(self.content, 'utf-8')
|
|
39
41
|
elif isinstance(self.content, bytes):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
filedata = self.content
|
|
43
|
+
else:
|
|
44
|
+
# Handle case where content is not str or bytes (e.g., JSON serialized)
|
|
45
|
+
filedata = bytes(self.content, 'utf-8')
|
|
46
|
+
|
|
47
|
+
return (self.file_name, filedata)
|
qase/commons/models/basemodel.py
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
class BaseModel:
|
|
5
|
-
def __str__(self) -> str:
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
def __str__(self, enum_as_name=False) -> str:
|
|
8
|
+
def serialize(o):
|
|
9
|
+
if isinstance(o, Enum):
|
|
10
|
+
return o.name if enum_as_name else o.value
|
|
11
|
+
elif hasattr(o, '__dict__'):
|
|
12
|
+
return o.__dict__
|
|
13
|
+
else:
|
|
14
|
+
return str(o)
|
|
15
|
+
|
|
16
|
+
return json.dumps(self, default=serialize, indent=4, sort_keys=True)
|
|
@@ -17,11 +17,28 @@ class Trace(Enum):
|
|
|
17
17
|
failed = "retain-on-failure"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
class XFailStatus(BaseModel):
|
|
21
|
+
xfail: str = None
|
|
22
|
+
xpass: str = None
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.xfail = 'skipped'
|
|
26
|
+
self.xpass = 'passed'
|
|
27
|
+
|
|
28
|
+
def set_xfail(self, value: str):
|
|
29
|
+
self.xfail = value
|
|
30
|
+
|
|
31
|
+
def set_xpass(self, value: str):
|
|
32
|
+
self.xpass = value
|
|
33
|
+
|
|
34
|
+
|
|
20
35
|
class PytestConfig(BaseModel):
|
|
21
36
|
capture_logs: bool = None
|
|
37
|
+
xfail_status: XFailStatus = None
|
|
22
38
|
|
|
23
39
|
def __init__(self):
|
|
24
40
|
self.capture_logs = False
|
|
41
|
+
self.xfail_status = XFailStatus()
|
|
25
42
|
|
|
26
43
|
def set_capture_logs(self, capture_logs):
|
|
27
44
|
self.capture_logs = QaseUtils.parse_bool(capture_logs)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
from typing import List, Dict, Optional
|
|
2
3
|
|
|
3
4
|
from .framework import Framework
|
|
4
5
|
from .report import ReportConfig
|
|
@@ -13,6 +14,21 @@ class Mode(Enum):
|
|
|
13
14
|
off = "off"
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
class LoggingConfig(BaseModel):
|
|
18
|
+
console: Optional[bool] = None
|
|
19
|
+
file: Optional[bool] = None
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.console = None
|
|
23
|
+
self.file = None
|
|
24
|
+
|
|
25
|
+
def set_console(self, console: bool):
|
|
26
|
+
self.console = console
|
|
27
|
+
|
|
28
|
+
def set_file(self, file: bool):
|
|
29
|
+
self.file = file
|
|
30
|
+
|
|
31
|
+
|
|
16
32
|
class ExecutionPlan(BaseModel):
|
|
17
33
|
path: str = None
|
|
18
34
|
|
|
@@ -34,6 +50,9 @@ class QaseConfig(BaseModel):
|
|
|
34
50
|
report: ReportConfig = None
|
|
35
51
|
profilers: list = None
|
|
36
52
|
framework: Framework = None
|
|
53
|
+
exclude_params: list = None
|
|
54
|
+
status_mapping: Dict[str, str] = None
|
|
55
|
+
logging: LoggingConfig = None
|
|
37
56
|
|
|
38
57
|
def __init__(self):
|
|
39
58
|
self.mode = Mode.off
|
|
@@ -44,6 +63,9 @@ class QaseConfig(BaseModel):
|
|
|
44
63
|
self.execution_plan = ExecutionPlan()
|
|
45
64
|
self.framework = Framework()
|
|
46
65
|
self.profilers = []
|
|
66
|
+
self.exclude_params = []
|
|
67
|
+
self.status_mapping = {}
|
|
68
|
+
self.logging = LoggingConfig()
|
|
47
69
|
|
|
48
70
|
def set_mode(self, mode: str):
|
|
49
71
|
if any(mode == e.value for e in Mode.__members__.values()):
|
|
@@ -64,3 +86,15 @@ class QaseConfig(BaseModel):
|
|
|
64
86
|
|
|
65
87
|
def set_debug(self, debug):
|
|
66
88
|
self.debug = QaseUtils.parse_bool(debug)
|
|
89
|
+
|
|
90
|
+
def set_exclude_params(self, exclude_params: List[str]):
|
|
91
|
+
self.exclude_params = exclude_params
|
|
92
|
+
|
|
93
|
+
def set_status_mapping(self, status_mapping: Dict[str, str]):
|
|
94
|
+
self.status_mapping = status_mapping
|
|
95
|
+
|
|
96
|
+
def set_logging(self, logging_config: dict):
|
|
97
|
+
if logging_config.get("console") is not None:
|
|
98
|
+
self.logging.set_console(QaseUtils.parse_bool(logging_config.get("console")))
|
|
99
|
+
if logging_config.get("file") is not None:
|
|
100
|
+
self.logging.set_file(QaseUtils.parse_bool(logging_config.get("file")))
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
1
2
|
from ..basemodel import BaseModel
|
|
3
|
+
from ..external_link import ExternalLinkConfig
|
|
2
4
|
from ... import QaseUtils
|
|
3
5
|
|
|
4
6
|
|
|
@@ -7,9 +9,14 @@ class RunConfig(BaseModel):
|
|
|
7
9
|
description: str = None
|
|
8
10
|
complete: bool = None
|
|
9
11
|
id: int = None
|
|
12
|
+
tags: List[str] = None
|
|
13
|
+
external_link: Optional[ExternalLinkConfig] = None
|
|
14
|
+
|
|
10
15
|
|
|
11
16
|
def __init__(self):
|
|
12
17
|
self.complete = True
|
|
18
|
+
self.tags = []
|
|
19
|
+
self.external_link = None
|
|
13
20
|
|
|
14
21
|
def set_title(self, title: str):
|
|
15
22
|
self.title = title
|
|
@@ -22,3 +29,15 @@ class RunConfig(BaseModel):
|
|
|
22
29
|
|
|
23
30
|
def set_id(self, id: int):
|
|
24
31
|
self.id = id
|
|
32
|
+
|
|
33
|
+
def set_tags(self, tags: List[str]):
|
|
34
|
+
self.tags = tags
|
|
35
|
+
|
|
36
|
+
def set_external_link(self, external_link: dict):
|
|
37
|
+
"""Set external link configuration from dictionary"""
|
|
38
|
+
if external_link:
|
|
39
|
+
self.external_link = ExternalLinkConfig()
|
|
40
|
+
if 'type' in external_link:
|
|
41
|
+
self.external_link.set_type(external_link['type'])
|
|
42
|
+
if 'link' in external_link:
|
|
43
|
+
self.external_link.set_link(external_link['link'])
|
|
@@ -4,6 +4,40 @@ from .plan import PlanConfig
|
|
|
4
4
|
from .run import RunConfig
|
|
5
5
|
from ..basemodel import BaseModel
|
|
6
6
|
from ... import QaseUtils
|
|
7
|
+
from typing import List
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigurationValue(BaseModel):
|
|
11
|
+
name: str = None
|
|
12
|
+
value: str = None
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str = None, value: str = None):
|
|
15
|
+
self.name = name
|
|
16
|
+
self.value = value
|
|
17
|
+
|
|
18
|
+
def set_name(self, name: str):
|
|
19
|
+
self.name = name
|
|
20
|
+
|
|
21
|
+
def set_value(self, value: str):
|
|
22
|
+
self.value = value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigurationsConfig(BaseModel):
|
|
26
|
+
values: List[ConfigurationValue] = None
|
|
27
|
+
create_if_not_exists: bool = None
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.values = []
|
|
31
|
+
self.create_if_not_exists = False
|
|
32
|
+
|
|
33
|
+
def set_values(self, values: List[ConfigurationValue]):
|
|
34
|
+
self.values = values
|
|
35
|
+
|
|
36
|
+
def set_create_if_not_exists(self, create_if_not_exists):
|
|
37
|
+
self.create_if_not_exists = QaseUtils.parse_bool(create_if_not_exists)
|
|
38
|
+
|
|
39
|
+
def add_value(self, name: str, value: str):
|
|
40
|
+
self.values.append(ConfigurationValue(name=name, value=value))
|
|
7
41
|
|
|
8
42
|
|
|
9
43
|
class TestopsConfig(BaseModel):
|
|
@@ -13,14 +47,19 @@ class TestopsConfig(BaseModel):
|
|
|
13
47
|
run: RunConfig = None
|
|
14
48
|
plan: PlanConfig = None
|
|
15
49
|
batch: BatchConfig = None
|
|
50
|
+
configurations: ConfigurationsConfig = None
|
|
51
|
+
status_filter: List[str] = None
|
|
52
|
+
show_public_report_link: bool = None
|
|
16
53
|
|
|
17
54
|
def __init__(self):
|
|
18
55
|
self.api = ApiConfig()
|
|
19
56
|
self.run = RunConfig()
|
|
20
57
|
self.batch = BatchConfig()
|
|
21
58
|
self.plan = PlanConfig()
|
|
59
|
+
self.configurations = ConfigurationsConfig()
|
|
22
60
|
self.defect = False
|
|
23
|
-
self.
|
|
61
|
+
self.status_filter = []
|
|
62
|
+
self.show_public_report_link = False
|
|
24
63
|
|
|
25
64
|
def set_project(self, project: str):
|
|
26
65
|
self.project = project
|
|
@@ -28,5 +67,8 @@ class TestopsConfig(BaseModel):
|
|
|
28
67
|
def set_defect(self, defect):
|
|
29
68
|
self.defect = QaseUtils.parse_bool(defect)
|
|
30
69
|
|
|
31
|
-
def
|
|
32
|
-
self.
|
|
70
|
+
def set_status_filter(self, status_filter: List[str]):
|
|
71
|
+
self.status_filter = status_filter
|
|
72
|
+
|
|
73
|
+
def set_show_public_report_link(self, show_public_report_link):
|
|
74
|
+
self.show_public_report_link = QaseUtils.parse_bool(show_public_report_link)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from .basemodel import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ExternalLinkType(Enum):
|
|
7
|
+
"""External link types supported by Qase TestOps"""
|
|
8
|
+
JIRA_CLOUD = 'jiraCloud'
|
|
9
|
+
JIRA_SERVER = 'jiraServer'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExternalLinkConfig(BaseModel):
|
|
13
|
+
"""Configuration for external link"""
|
|
14
|
+
type: ExternalLinkType = None
|
|
15
|
+
link: str = None
|
|
16
|
+
|
|
17
|
+
def __init__(self, type: ExternalLinkType = None, link: str = None):
|
|
18
|
+
self.type = type
|
|
19
|
+
self.link = link
|
|
20
|
+
|
|
21
|
+
def set_type(self, type: str):
|
|
22
|
+
"""Set external link type from string"""
|
|
23
|
+
if type == 'jiraCloud':
|
|
24
|
+
self.type = ExternalLinkType.JIRA_CLOUD
|
|
25
|
+
elif type == 'jiraServer':
|
|
26
|
+
self.type = ExternalLinkType.JIRA_SERVER
|
|
27
|
+
else:
|
|
28
|
+
raise ValueError(f"Invalid external link type: {type}. Supported types: jiraCloud, jiraServer")
|
|
29
|
+
|
|
30
|
+
def set_link(self, link: str):
|
|
31
|
+
"""Set external link URL or identifier"""
|
|
32
|
+
self.link = link
|
|
33
|
+
|
|
34
|
+
def to_api_type(self) -> str:
|
|
35
|
+
"""Convert to API enum value"""
|
|
36
|
+
if self.type == ExternalLinkType.JIRA_CLOUD:
|
|
37
|
+
return 'jira-cloud'
|
|
38
|
+
elif self.type == ExternalLinkType.JIRA_SERVER:
|
|
39
|
+
return 'jira-server'
|
|
40
|
+
else:
|
|
41
|
+
raise ValueError(f"Invalid external link type: {self.type}")
|
qase/commons/models/relation.py
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
from .basemodel import BaseModel
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
def __init__(self,
|
|
6
|
-
self.
|
|
4
|
+
class SuiteData(BaseModel):
|
|
5
|
+
def __init__(self, title: str) -> None:
|
|
6
|
+
self.public_id = None
|
|
7
7
|
self.title = title
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class RelationSuite(BaseModel):
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self.data = []
|
|
13
|
+
|
|
14
|
+
def add_data(self, data: SuiteData) -> None:
|
|
15
|
+
self.data.append(data)
|
|
16
|
+
|
|
17
|
+
|
|
10
18
|
class Relation(BaseModel):
|
|
11
|
-
def __init__(self
|
|
12
|
-
self.
|
|
13
|
-
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.suite = RelationSuite()
|
|
21
|
+
|
|
22
|
+
def add_suite(self, suite: SuiteData) -> None:
|
|
23
|
+
self.suite.add_data(suite)
|
qase/commons/models/result.py
CHANGED
|
@@ -4,7 +4,6 @@ import uuid
|
|
|
4
4
|
from typing import Type, Optional, Union, Dict, List
|
|
5
5
|
from .basemodel import BaseModel
|
|
6
6
|
from .step import Step
|
|
7
|
-
from .suite import Suite
|
|
8
7
|
from .attachment import Attachment
|
|
9
8
|
from .relation import Relation
|
|
10
9
|
from .. import QaseUtils
|
|
@@ -26,7 +25,7 @@ class Execution(BaseModel):
|
|
|
26
25
|
stacktrace: Optional[str] = None,
|
|
27
26
|
thread: Optional[str] = QaseUtils.get_thread_name()
|
|
28
27
|
):
|
|
29
|
-
self.start_time =
|
|
28
|
+
self.start_time = QaseUtils.get_real_time()
|
|
30
29
|
self.status = status
|
|
31
30
|
self.end_time = end_time
|
|
32
31
|
self.duration = duration
|
|
@@ -34,16 +33,22 @@ class Execution(BaseModel):
|
|
|
34
33
|
self.thread = thread
|
|
35
34
|
|
|
36
35
|
def set_status(self, status: Optional[str]):
|
|
37
|
-
if status
|
|
36
|
+
if status is None:
|
|
38
37
|
self.status = status
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
# Convert to lowercase for validation
|
|
41
|
+
status_lower = status.lower()
|
|
42
|
+
if status_lower in ['passed', 'failed', 'skipped', 'untested', 'invalid', 'disabled', 'blocked']:
|
|
43
|
+
self.status = status_lower
|
|
39
44
|
else:
|
|
40
|
-
raise ValueError('Step status must be one of: passed, failed, skipped, untested')
|
|
45
|
+
raise ValueError('Step status must be one of: passed, failed, skipped, untested, invalid, disabled, blocked')
|
|
41
46
|
|
|
42
47
|
def get_status(self):
|
|
43
48
|
return self.status
|
|
44
49
|
|
|
45
50
|
def complete(self):
|
|
46
|
-
self.end_time =
|
|
51
|
+
self.end_time = QaseUtils.get_real_time()
|
|
47
52
|
self.duration = (int)((self.end_time - self.start_time) * 1000)
|
|
48
53
|
|
|
49
54
|
|
|
@@ -70,20 +75,16 @@ class Result(BaseModel):
|
|
|
70
75
|
self.id: str = str(uuid.uuid4())
|
|
71
76
|
self.title: str = title
|
|
72
77
|
self.signature: str = signature
|
|
73
|
-
self.
|
|
74
|
-
self.testops_id: Optional[int] = None
|
|
78
|
+
self.testops_ids: Optional[List[int]] = None
|
|
75
79
|
self.execution: Type[Execution] = Execution()
|
|
76
80
|
self.fields: Dict[Type[Field]] = {}
|
|
77
81
|
self.attachments: List[Attachment] = []
|
|
78
82
|
self.steps: List[Type[Step]] = []
|
|
79
83
|
self.params: Optional[dict] = {}
|
|
80
84
|
self.param_groups: Optional[List[List[str]]] = []
|
|
81
|
-
self.
|
|
82
|
-
self.relations: List[Type[Relation]] = []
|
|
85
|
+
self.relations: Type[Relation] = None
|
|
83
86
|
self.muted: bool = False
|
|
84
87
|
self.message: Optional[str] = None
|
|
85
|
-
self.suite: Optional[Type[Suite]] = None
|
|
86
|
-
QaseUtils.get_host_data()
|
|
87
88
|
|
|
88
89
|
def add_message(self, message: str) -> None:
|
|
89
90
|
self.message = message
|
|
@@ -97,20 +98,14 @@ class Result(BaseModel):
|
|
|
97
98
|
def add_attachment(self, attachment: Attachment) -> None:
|
|
98
99
|
self.attachments.append(attachment)
|
|
99
100
|
|
|
100
|
-
def add_relation(self, relation: Type[Relation]) -> None:
|
|
101
|
-
self.relations.append(relation)
|
|
102
|
-
|
|
103
101
|
def add_param(self, key: str, value: str) -> None:
|
|
104
102
|
self.params[key] = value
|
|
105
103
|
|
|
106
104
|
def add_param_groups(self, values: List[str]) -> None:
|
|
107
105
|
self.param_groups.append(values)
|
|
108
106
|
|
|
109
|
-
def
|
|
110
|
-
self.relations
|
|
111
|
-
|
|
112
|
-
def add_suite(self, suite: Type[Suite]) -> None:
|
|
113
|
-
self.suite = suite
|
|
107
|
+
def set_relation(self, relation: Relation) -> None:
|
|
108
|
+
self.relations = relation
|
|
114
109
|
|
|
115
110
|
def get_status(self) -> Optional[str]:
|
|
116
111
|
return self.execution.status
|
|
@@ -126,18 +121,8 @@ class Result(BaseModel):
|
|
|
126
121
|
return self.fields[name]
|
|
127
122
|
return None
|
|
128
123
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
# Hack for old API
|
|
132
|
-
return 0
|
|
133
|
-
return self.testops_id
|
|
124
|
+
def get_testops_ids(self) -> Optional[List[int]]:
|
|
125
|
+
return self.testops_ids
|
|
134
126
|
|
|
135
127
|
def get_duration(self) -> int:
|
|
136
128
|
return self.execution.duration
|
|
137
|
-
|
|
138
|
-
def get_suite_title(self) -> Optional[str]:
|
|
139
|
-
if self.suite:
|
|
140
|
-
return self.suite.title
|
|
141
|
-
|
|
142
|
-
def set_run_id(self, run_id: str) -> None:
|
|
143
|
-
self.run_id = run_id
|
qase/commons/models/run.py
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
3
1
|
from typing import Optional, List
|
|
4
2
|
|
|
5
3
|
from .basemodel import BaseModel
|
|
@@ -26,6 +24,7 @@ class RunStats(BaseModel):
|
|
|
26
24
|
self.failed = 0
|
|
27
25
|
self.skipped = 0
|
|
28
26
|
self.broken = 0
|
|
27
|
+
self.invalid = 0
|
|
29
28
|
self.muted = 0
|
|
30
29
|
self.total = 0
|
|
31
30
|
|
|
@@ -39,6 +38,8 @@ class RunStats(BaseModel):
|
|
|
39
38
|
self.skipped += 1
|
|
40
39
|
elif status == "broken":
|
|
41
40
|
self.broken += 1
|
|
41
|
+
elif status == "invalid":
|
|
42
|
+
self.invalid += 1
|
|
42
43
|
self.total += 1
|
|
43
44
|
if result.get('muted', False):
|
|
44
45
|
self.muted += 1
|
|
@@ -71,6 +72,7 @@ class Run(BaseModel):
|
|
|
71
72
|
"duration": result["execution"]["duration"],
|
|
72
73
|
"thread": result["execution"]["thread"]
|
|
73
74
|
}
|
|
75
|
+
self._extract_path_from_relations(result)
|
|
74
76
|
self.results.append(compact_result)
|
|
75
77
|
self.execution.track(result)
|
|
76
78
|
self.stats.track(result)
|
|
@@ -79,3 +81,16 @@ class Run(BaseModel):
|
|
|
79
81
|
|
|
80
82
|
def add_host_data(self, host_data: dict):
|
|
81
83
|
self.host_data = host_data
|
|
84
|
+
|
|
85
|
+
def _extract_path_from_relations(self, relations_dict):
|
|
86
|
+
|
|
87
|
+
titles = []
|
|
88
|
+
if "relations" in relations_dict and "suite" in relations_dict["relations"]:
|
|
89
|
+
if "data" in relations_dict["relations"]["suite"]:
|
|
90
|
+
data_list = relations_dict["relations"]["suite"]["data"]
|
|
91
|
+
titles = [item["title"] for item in data_list if "title" in item]
|
|
92
|
+
|
|
93
|
+
path = "/".join(titles)
|
|
94
|
+
|
|
95
|
+
if path and path not in self.suites:
|
|
96
|
+
self.suites.append(path)
|
qase/commons/models/runtime.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from . import Result
|
|
1
2
|
from .step import Step
|
|
2
3
|
from .attachment import Attachment
|
|
3
4
|
|
|
@@ -42,6 +43,14 @@ class Runtime:
|
|
|
42
43
|
except Exception as e:
|
|
43
44
|
raise QaseRuntimeException(e)
|
|
44
45
|
|
|
46
|
+
|
|
47
|
+
def add_param(self, key: str, value: str):
|
|
48
|
+
"""
|
|
49
|
+
Add a parameter to the current result.
|
|
50
|
+
"""
|
|
51
|
+
if self.result is not None:
|
|
52
|
+
self.result.params[key] = value
|
|
53
|
+
|
|
45
54
|
def clear(self):
|
|
46
55
|
self.result = None
|
|
47
56
|
self.steps = {}
|