testbench2robotframework 0.8.0a3__tar.gz → 0.8.0a5__tar.gz
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.
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/.gitignore +8 -1
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/PKG-INFO +1 -1
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/__init__.py +1 -1
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/cli.py +37 -16
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/config.py +23 -3
- testbench2robotframework-0.8.0a5/testbench2robotframework/execution_artifacts.py +245 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/json_reader.py +9 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/json_writer.py +12 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/model.py +16 -8
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/result_writer.py +27 -92
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testbench2rf.py +69 -38
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/utils.py +4 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/CreatePiPWheel.bat +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/CreatePiPWheel.sh +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/DEVELOPMENT.md +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/json_config.json +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/pyproject_example.toml +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/toml_config.toml +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/LICENSE +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/MANIFEST.in +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/README.md +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/json_config_tests/1_tfs.robot +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/libs/json_config.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/libs/pyproject_config.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/resources/file_management.resource +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/resources/testbench2robotframework_cli.resource +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/json_config.robot +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/no_config_argument.robot +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/toml_config.robot +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/create_json_schema.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/LibrarySubdivision.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/Unbenannt.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/generated.png +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/libraries.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/resources.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/rfLibraryRootsTestBench.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testbench_rfLibraryRegex.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testbench_rfResourceRegex.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testthemen.PNG +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/oldModel.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/pydantic_model.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/pyproject.toml +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/requirements.txt +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/robot.toml +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tasks.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench-tools.zip +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/__main__.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/html_parser.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/log.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/model_utils.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/robotframework2testbench.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testbench2robotframework.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testsuite_write.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_data/configurations/invalid_config.json +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_data/configurations/valid_config.json +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_missing_files.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_robot_files_should_not_contain_invalid_characters.py +0 -0
- {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_zip_file_generation.py +0 -0
|
@@ -11,9 +11,12 @@ from .config import (
|
|
|
11
11
|
DEFAULT_GENERATION_DIRECTORY,
|
|
12
12
|
DEFAULT_LIBRARY_REGEX,
|
|
13
13
|
DEFAULT_LIBRARY_ROOTS,
|
|
14
|
+
DEFAULT_RESOURCE_DIRECTORY_REGEX,
|
|
14
15
|
DEFAULT_RESOURCE_REGEX,
|
|
15
16
|
DEFAULT_RESOURCE_ROOTS,
|
|
17
|
+
find_private_robot_toml,
|
|
16
18
|
find_pyproject_toml,
|
|
19
|
+
find_robot_toml,
|
|
17
20
|
get_testbench2robotframework_toml_dict,
|
|
18
21
|
)
|
|
19
22
|
from .json_reader import read_json
|
|
@@ -72,8 +75,8 @@ def testbench2robotframework_cli():
|
|
|
72
75
|
@click.option(
|
|
73
76
|
"--fully-qualified",
|
|
74
77
|
is_flag=True,
|
|
75
|
-
help="""
|
|
76
|
-
|
|
78
|
+
help="""Calls Robot Framework keywords by their fully
|
|
79
|
+
qualified names in the generated test suites.""",
|
|
77
80
|
)
|
|
78
81
|
@click.option(
|
|
79
82
|
"-d",
|
|
@@ -94,12 +97,20 @@ def testbench2robotframework_cli():
|
|
|
94
97
|
type=click.Path(path_type=Path),
|
|
95
98
|
help="Directory containing the Robot Framework resource files.",
|
|
96
99
|
)
|
|
100
|
+
@click.option(
|
|
101
|
+
"--resource-directory-regex",
|
|
102
|
+
type=str,
|
|
103
|
+
help="""Regex that can be used to identify the TestBench
|
|
104
|
+
Subdivision that corresponds to the <resource-directory>.
|
|
105
|
+
Resources will be imported relative to this Subdivision
|
|
106
|
+
based on the test elements structure in TestBench.""",
|
|
107
|
+
)
|
|
97
108
|
@click.option(
|
|
98
109
|
"--library-regex",
|
|
99
110
|
multiple=True,
|
|
100
111
|
type=str,
|
|
101
|
-
help="""
|
|
102
|
-
|
|
112
|
+
help="""Regular expression used to identify TestBench subdivisions
|
|
113
|
+
corresponding to Robot Framework libraries.""",
|
|
103
114
|
)
|
|
104
115
|
@click.option(
|
|
105
116
|
"--library-root",
|
|
@@ -112,8 +123,8 @@ def testbench2robotframework_cli():
|
|
|
112
123
|
"--resource-regex",
|
|
113
124
|
multiple=True,
|
|
114
125
|
type=str,
|
|
115
|
-
help="""
|
|
116
|
-
|
|
126
|
+
help="""Regular expression used to identify TestBench subdivisions
|
|
127
|
+
corresponding to Robot Framework resources.""",
|
|
117
128
|
)
|
|
118
129
|
@click.option(
|
|
119
130
|
"--resource-root",
|
|
@@ -126,13 +137,15 @@ def testbench2robotframework_cli():
|
|
|
126
137
|
"--library-mapping",
|
|
127
138
|
multiple=True,
|
|
128
139
|
callback=parse_subdivision_mapping,
|
|
129
|
-
help=""
|
|
140
|
+
help="""Library import statement to use when a keyword from the
|
|
141
|
+
specified TestBench subdivision is encountered.""",
|
|
130
142
|
)
|
|
131
143
|
@click.option(
|
|
132
144
|
"--resource-mapping",
|
|
133
145
|
multiple=True,
|
|
134
146
|
callback=parse_subdivision_mapping,
|
|
135
|
-
help=""
|
|
147
|
+
help="""Resource import statement to use when a keyword from the
|
|
148
|
+
specified TestBench subdivision is encountered.""",
|
|
136
149
|
)
|
|
137
150
|
@click.argument("testbench-report", type=click.Path(path_type=Path))
|
|
138
151
|
def generate_tests( # noqa: PLR0913
|
|
@@ -141,6 +154,7 @@ def generate_tests( # noqa: PLR0913
|
|
|
141
154
|
config: Path,
|
|
142
155
|
fully_qualified: bool,
|
|
143
156
|
library_regex: tuple[str],
|
|
157
|
+
resource_directory_regex: str,
|
|
144
158
|
library_root: tuple[str],
|
|
145
159
|
log_suite_numbering: bool,
|
|
146
160
|
output_directory: Path,
|
|
@@ -180,7 +194,10 @@ def generate_tests( # noqa: PLR0913
|
|
|
180
194
|
configuration["resource-directory"] = (
|
|
181
195
|
resource_directory.as_posix()
|
|
182
196
|
if resource_directory
|
|
183
|
-
else configuration.get("resource-directory", "")
|
|
197
|
+
else configuration.get("resource-directory", "resources")
|
|
198
|
+
)
|
|
199
|
+
configuration["resource-directory-regex"] = resource_directory_regex or configuration.get(
|
|
200
|
+
"resource-directory-regex", DEFAULT_RESOURCE_DIRECTORY_REGEX
|
|
184
201
|
)
|
|
185
202
|
configuration["resource-mapping"] = resource_mapping or configuration.get(
|
|
186
203
|
"resource-mapping", {}
|
|
@@ -216,11 +233,15 @@ def fetch_results(config: Path, robot_result: Path, output_directory: Path, test
|
|
|
216
233
|
def get_tb2robot_file_configuration(config: Path) -> dict:
|
|
217
234
|
if not config:
|
|
218
235
|
pyproject_toml = find_pyproject_toml()
|
|
219
|
-
|
|
236
|
+
robot_toml = find_robot_toml()
|
|
237
|
+
private_robot_toml = find_private_robot_toml()
|
|
238
|
+
pyproject_config = get_testbench2robotframework_toml_dict(pyproject_toml)
|
|
239
|
+
robot_config = get_testbench2robotframework_toml_dict(robot_toml)
|
|
240
|
+
private_robot_config = get_testbench2robotframework_toml_dict(private_robot_toml)
|
|
241
|
+
return {**pyproject_config, **robot_config, **private_robot_config}
|
|
242
|
+
config_path = config
|
|
220
243
|
if not config_path:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
configuration = get_testbench2robotframework_toml_dict(config_path)
|
|
226
|
-
return configuration
|
|
244
|
+
return {}
|
|
245
|
+
if config_path.suffix == ".json":
|
|
246
|
+
return read_json(config, False)
|
|
247
|
+
return get_testbench2robotframework_toml_dict(config_path)
|
|
@@ -18,6 +18,7 @@ DEFAULT_LIBRARY_ROOTS: Final[list[str]] = ["RF", "RF-Library"]
|
|
|
18
18
|
DEFAULT_RESOURCE_REGEX = r"(?:.*\.)?(?P<resourceName>[^.]+?)\s*\[Robot-Resource\].*"
|
|
19
19
|
DEFAULT_RESOURCE_ROOTS: Final[list[str]] = ["RF-Resource"]
|
|
20
20
|
DEFAULT_GENERATION_DIRECTORY = "{root}/Generated"
|
|
21
|
+
DEFAULT_RESOURCE_DIRECTORY_REGEX = r".*\[Robot-Resources\].*"
|
|
21
22
|
|
|
22
23
|
class StrEnum(str, Enum):
|
|
23
24
|
def __new__(cls, *args):
|
|
@@ -41,6 +42,22 @@ def find_pyproject_toml() -> Path:
|
|
|
41
42
|
return pyproject_path
|
|
42
43
|
return Path()
|
|
43
44
|
|
|
45
|
+
def find_robot_toml() -> Path:
|
|
46
|
+
current_dir = Path().cwd()
|
|
47
|
+
for parent in [current_dir, *list(current_dir.parents)]:
|
|
48
|
+
robot_path = parent / "robot.toml"
|
|
49
|
+
if robot_path.is_file():
|
|
50
|
+
return robot_path
|
|
51
|
+
return Path()
|
|
52
|
+
|
|
53
|
+
def find_private_robot_toml() -> Path:
|
|
54
|
+
current_dir = Path().cwd()
|
|
55
|
+
for parent in [current_dir, *list(current_dir.parents)]:
|
|
56
|
+
private_robot_path = parent / ".robot.toml"
|
|
57
|
+
if private_robot_path.is_file():
|
|
58
|
+
return private_robot_path
|
|
59
|
+
return Path()
|
|
60
|
+
|
|
44
61
|
|
|
45
62
|
def get_testbench2robotframework_toml_dict(toml_path: Path):
|
|
46
63
|
try:
|
|
@@ -180,6 +197,7 @@ class Configuration:
|
|
|
180
197
|
phasePattern: str
|
|
181
198
|
referenceBehaviour: ReferenceBehaviour
|
|
182
199
|
resource_directory: str
|
|
200
|
+
resource_directory_regex: str
|
|
183
201
|
resource_regex: list[str]
|
|
184
202
|
resource_root: list[str]
|
|
185
203
|
subdivisionsMapping: SubdivisionsMapping
|
|
@@ -192,6 +210,8 @@ class Configuration:
|
|
|
192
210
|
library_regex=dictionary.get(
|
|
193
211
|
"library-regex", [DEFAULT_LIBRARY_REGEX]
|
|
194
212
|
),
|
|
213
|
+
resource_directory_regex=dictionary.get(
|
|
214
|
+
"resource-directory-regex", DEFAULT_RESOURCE_DIRECTORY_REGEX),
|
|
195
215
|
resource_regex=dictionary.get(
|
|
196
216
|
"resource-regex", [DEFAULT_RESOURCE_REGEX]
|
|
197
217
|
),
|
|
@@ -211,10 +231,10 @@ class Configuration:
|
|
|
211
231
|
resource_directory=dictionary.get("resource-directory", "").replace(
|
|
212
232
|
"\\", "/"
|
|
213
233
|
),
|
|
214
|
-
testCaseSplitPathRegEx=dictionary.get("
|
|
234
|
+
testCaseSplitPathRegEx=dictionary.get("testcase-splitting-regex", ".*StopWithRestart.*"),
|
|
215
235
|
phasePattern=dictionary.get("phasePattern", "{testcase} : Phase {index}/{length}"),
|
|
216
236
|
referenceBehaviour=ReferenceBehaviour(
|
|
217
|
-
dictionary.get("
|
|
237
|
+
dictionary.get("reference-behaviour", "ATTACHMENT").upper()
|
|
218
238
|
),
|
|
219
239
|
subdivisionsMapping=SubdivisionsMapping.from_dict(
|
|
220
240
|
{
|
|
@@ -223,7 +243,7 @@ class Configuration:
|
|
|
223
243
|
}
|
|
224
244
|
),
|
|
225
245
|
attachmentConflictBehaviour=AttachmentConflictBehaviour(
|
|
226
|
-
dictionary.get("
|
|
246
|
+
dictionary.get("attachment-conflict-behaviour", "USE_EXISTING").upper()
|
|
227
247
|
),
|
|
228
248
|
)
|
|
229
249
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from urllib.parse import unquote
|
|
6
|
+
|
|
7
|
+
from .config import AttachmentConflictBehaviour, ReferenceBehaviour
|
|
8
|
+
from .log import logger
|
|
9
|
+
from .model import ReferenceAssignment, ReferenceKind
|
|
10
|
+
|
|
11
|
+
FILE_URI_SCHEME = "file://"
|
|
12
|
+
MEGABYTE = 1000 * 1000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExecutionArtifactStorage:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
reference_behaviour: ReferenceBehaviour,
|
|
19
|
+
attachment_conflict_behaviour: AttachmentConflictBehaviour,
|
|
20
|
+
tb_references: list[ReferenceAssignment],
|
|
21
|
+
output_xml: str,
|
|
22
|
+
attachment_folder: str,
|
|
23
|
+
):
|
|
24
|
+
self.reference_behaviour = reference_behaviour
|
|
25
|
+
self.attachmentConflictBehaviour = attachment_conflict_behaviour
|
|
26
|
+
self.tb_references: list[ReferenceAssignment] = tb_references
|
|
27
|
+
self.output_xml = output_xml
|
|
28
|
+
self.attachment_folder = attachment_folder
|
|
29
|
+
self._key: Optional[int] = None
|
|
30
|
+
|
|
31
|
+
def add_artifact(self, artifact: str) -> Optional[str]:
|
|
32
|
+
"""
|
|
33
|
+
Adds an artifact to the storage based on the reference behaviour.
|
|
34
|
+
|
|
35
|
+
:param artifact: The artifact to add.
|
|
36
|
+
:type artifact: str
|
|
37
|
+
:return: The key of the added artifact or None if it could not be added.
|
|
38
|
+
:rtype: Optional[str]
|
|
39
|
+
"""
|
|
40
|
+
artifact_value = self._dispatch_artifact_processing(artifact)
|
|
41
|
+
if not artifact_value:
|
|
42
|
+
return None
|
|
43
|
+
existing_artifact = self._get_existing_artifact(artifact_value, self.reference_behaviour)
|
|
44
|
+
if existing_artifact:
|
|
45
|
+
# if self.reference_behaviour == ReferenceBehaviour.ATTACHMENT:
|
|
46
|
+
# self._handle_attachment_conflict(existing_artifact, artifact_value)
|
|
47
|
+
return existing_artifact.key
|
|
48
|
+
return self._add_new_reference(artifact_value, self.reference_behaviour)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def new_key(self) -> int:
|
|
52
|
+
if self._key is not None:
|
|
53
|
+
self._key -= 1
|
|
54
|
+
return self._key
|
|
55
|
+
if not self.tb_references:
|
|
56
|
+
self._key = -4
|
|
57
|
+
return self._key
|
|
58
|
+
min_key = min([int(ref.key) for ref in self.tb_references])
|
|
59
|
+
starting_key_new_references = -3
|
|
60
|
+
self._key = (
|
|
61
|
+
min_key if min_key < starting_key_new_references else starting_key_new_references
|
|
62
|
+
)
|
|
63
|
+
self._key -= 1
|
|
64
|
+
return self._key
|
|
65
|
+
|
|
66
|
+
def _create_unique_attachment_path(self, attachement_path: Path) -> Path:
|
|
67
|
+
counter = 1
|
|
68
|
+
attachment_stem = attachement_path.stem
|
|
69
|
+
while attachement_path.exists():
|
|
70
|
+
attachement_path = Path(
|
|
71
|
+
f"{attachement_path.parent}",
|
|
72
|
+
f"{attachment_stem}_{counter}{attachement_path.suffix}",
|
|
73
|
+
)
|
|
74
|
+
counter += 1
|
|
75
|
+
return attachement_path
|
|
76
|
+
|
|
77
|
+
def _dispatch_attachment_copy(
|
|
78
|
+
self, filename: str, artifact_value: str, attachment_folder_path: Path
|
|
79
|
+
) -> str:
|
|
80
|
+
conflict_methods = {
|
|
81
|
+
AttachmentConflictBehaviour.USE_NEW: (
|
|
82
|
+
self._use_new_attachment,
|
|
83
|
+
[filename, artifact_value, attachment_folder_path],
|
|
84
|
+
),
|
|
85
|
+
AttachmentConflictBehaviour.RENAME_NEW: (
|
|
86
|
+
self._rename_new_attachment,
|
|
87
|
+
[filename, artifact_value, attachment_folder_path],
|
|
88
|
+
),
|
|
89
|
+
AttachmentConflictBehaviour.USE_EXISTING: (self._use_existing_attachment, [filename]),
|
|
90
|
+
AttachmentConflictBehaviour.ERROR: (self._log_attachment_error, [filename]),
|
|
91
|
+
}
|
|
92
|
+
method, args = conflict_methods.get(
|
|
93
|
+
self.attachmentConflictBehaviour, (self._invalid_conflict_behaviour, [])
|
|
94
|
+
)
|
|
95
|
+
return method(*args)
|
|
96
|
+
|
|
97
|
+
def _use_new_attachment(
|
|
98
|
+
self, filename: str, artifact_value: str, attachment_folder_path: Path
|
|
99
|
+
) -> str:
|
|
100
|
+
shutil.copyfile(artifact_value, attachment_folder_path / filename, follow_symlinks=True)
|
|
101
|
+
return filename
|
|
102
|
+
|
|
103
|
+
def _rename_new_attachment(
|
|
104
|
+
self, filename: str, artifact_value: str, attachment_folder_path: Path
|
|
105
|
+
) -> str:
|
|
106
|
+
unique_path = self._create_unique_attachment_path(attachment_folder_path / filename)
|
|
107
|
+
unique_file = Path(unique_path).name
|
|
108
|
+
logger.info(
|
|
109
|
+
f"Attachment '{filename}' does already exist. "
|
|
110
|
+
f"Creating new unique attachment '{unique_file}'."
|
|
111
|
+
)
|
|
112
|
+
shutil.copyfile(artifact_value, unique_path, follow_symlinks=True)
|
|
113
|
+
return unique_file
|
|
114
|
+
|
|
115
|
+
def _use_existing_attachment(self, filename: str) -> str:
|
|
116
|
+
return filename
|
|
117
|
+
|
|
118
|
+
def _log_attachment_error(self, filename: str):
|
|
119
|
+
logger.error(f"Attachment '{filename}' does already exist.")
|
|
120
|
+
return filename
|
|
121
|
+
|
|
122
|
+
def _invalid_conflict_behaviour(self, filename: str):
|
|
123
|
+
logger.error(
|
|
124
|
+
f"Attachment conflict behaviour '{self.attachment_conflict_behaviour}' "
|
|
125
|
+
f"is not valid value. Please consult the documentation."
|
|
126
|
+
)
|
|
127
|
+
sys.exit()
|
|
128
|
+
|
|
129
|
+
def _copy_attachment(self, artifact_value: str) -> str:
|
|
130
|
+
filename = Path(artifact_value).name
|
|
131
|
+
attachment_folder_path = Path(self.attachment_folder)
|
|
132
|
+
if not attachment_folder_path.exists():
|
|
133
|
+
attachment_folder_path.mkdir(parents=True, exist_ok=True)
|
|
134
|
+
if not (attachment_folder_path / filename).exists():
|
|
135
|
+
return self._use_new_attachment(filename, artifact_value, attachment_folder_path)
|
|
136
|
+
return self._dispatch_attachment_copy(filename, artifact_value, attachment_folder_path)
|
|
137
|
+
|
|
138
|
+
def _process_artifact(self, artifact: str) -> Optional[str]:
|
|
139
|
+
artifact_info = ExecutionArtifactInfo(artifact, self.output_xml)
|
|
140
|
+
artifact_value = artifact_info.get_attachment_value()
|
|
141
|
+
if not artifact_value:
|
|
142
|
+
logger.warning(
|
|
143
|
+
f"Attachment '{artifact}' does not exist or can not be handled as an attachment."
|
|
144
|
+
)
|
|
145
|
+
return None
|
|
146
|
+
if not has_allowed_size(artifact_value):
|
|
147
|
+
logger.error(
|
|
148
|
+
f"Attachment '{artifact_value}' exceeds the maximum allowed size of 10 MB."
|
|
149
|
+
)
|
|
150
|
+
return None
|
|
151
|
+
return self._copy_attachment(artifact_value)
|
|
152
|
+
|
|
153
|
+
def _process_reference(self, artifact: str) -> Optional[str]:
|
|
154
|
+
artifact_info = ExecutionArtifactInfo(artifact, self.output_xml)
|
|
155
|
+
artifact_value = artifact_info.get_reference_value()
|
|
156
|
+
if not artifact_value:
|
|
157
|
+
logger.warning(
|
|
158
|
+
f"Reference '{artifact}' does not exist or can not be handled as a reference."
|
|
159
|
+
)
|
|
160
|
+
return None
|
|
161
|
+
return artifact_value
|
|
162
|
+
|
|
163
|
+
def _process_unknown(self, artifact: str) -> Optional[str]:
|
|
164
|
+
logger.error(
|
|
165
|
+
f"Unknown reference behaviour '{self.reference_behaviour}'."
|
|
166
|
+
f"Cannot add artifact '{artifact}'."
|
|
167
|
+
)
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def _process_no_references_allowed(self, artifact: str) -> Optional[str]:
|
|
171
|
+
logger.warning(
|
|
172
|
+
f"Reference behaviour is set to NONE."
|
|
173
|
+
f"Reference '{artifact}' will not be added to report."
|
|
174
|
+
)
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _dispatch_artifact_processing(self, artifact):
|
|
178
|
+
artifact_processing_methods = {
|
|
179
|
+
ReferenceBehaviour.NONE: self._process_no_references_allowed,
|
|
180
|
+
ReferenceBehaviour.ATTACHMENT: self._process_artifact,
|
|
181
|
+
ReferenceBehaviour.REFERENCE: self._process_reference,
|
|
182
|
+
}
|
|
183
|
+
return artifact_processing_methods.get(self.reference_behaviour, self._process_unknown)(
|
|
184
|
+
artifact
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def _add_new_reference(self, artifact_value: str, reference_behaviour: ReferenceBehaviour):
|
|
188
|
+
new_key = str(self.new_key)
|
|
189
|
+
self.tb_references.append(
|
|
190
|
+
ReferenceAssignment(
|
|
191
|
+
key=new_key,
|
|
192
|
+
value=artifact_value,
|
|
193
|
+
referenceType=ReferenceKind(reference_behaviour.capitalize()),
|
|
194
|
+
)
|
|
195
|
+
)
|
|
196
|
+
return new_key
|
|
197
|
+
|
|
198
|
+
def _get_existing_artifact(self, artifact_value: str, reference_behaviour: ReferenceBehaviour):
|
|
199
|
+
return next(
|
|
200
|
+
filter(
|
|
201
|
+
lambda ref: ref.value == artifact_value
|
|
202
|
+
and ref.referenceType.value.lower() == reference_behaviour.lower(),
|
|
203
|
+
self.tb_references,
|
|
204
|
+
),
|
|
205
|
+
None,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class ExecutionArtifactInfo:
|
|
210
|
+
def __init__(self, artifact: str, output_xml: str) -> None:
|
|
211
|
+
unquoted_artifact = unquote(artifact)
|
|
212
|
+
if unquoted_artifact.startswith(FILE_URI_SCHEME):
|
|
213
|
+
unquoted_artifact = unquoted_artifact[len(FILE_URI_SCHEME) :]
|
|
214
|
+
self.artifact = unquoted_artifact
|
|
215
|
+
self.output_xml = output_xml
|
|
216
|
+
|
|
217
|
+
def get_reference_value(self) -> Optional[str]:
|
|
218
|
+
unquoted_path = Path(self.artifact)
|
|
219
|
+
if not unquoted_path.exists():
|
|
220
|
+
robot_output_dir = Path(self.output_xml).parent
|
|
221
|
+
if Path(robot_output_dir, unquoted_path).exists():
|
|
222
|
+
unquoted_path = robot_output_dir / unquoted_path
|
|
223
|
+
unquoted_path = unquoted_path.resolve()
|
|
224
|
+
elif unquoted_path.is_absolute():
|
|
225
|
+
logger.warning(
|
|
226
|
+
f"Referenced file '{unquoted_path}' does not exist."
|
|
227
|
+
f"Reference will be attached anyway."
|
|
228
|
+
)
|
|
229
|
+
else:
|
|
230
|
+
return None
|
|
231
|
+
return str(unquoted_path)
|
|
232
|
+
|
|
233
|
+
def get_attachment_value(self) -> Optional[str]:
|
|
234
|
+
unquoted_path = Path(self.artifact)
|
|
235
|
+
if not unquoted_path.exists():
|
|
236
|
+
robot_output_dir = Path(self.output_xml).parent
|
|
237
|
+
if not Path(robot_output_dir, unquoted_path).exists():
|
|
238
|
+
return None
|
|
239
|
+
unquoted_path = robot_output_dir / unquoted_path
|
|
240
|
+
return str(unquoted_path)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def has_allowed_size(file: str) -> bool:
|
|
244
|
+
file_size = Path.stat(Path(file)).st_size
|
|
245
|
+
return not file_size >= 10 * MEGABYTE
|
|
@@ -7,6 +7,7 @@ from typing import Optional
|
|
|
7
7
|
|
|
8
8
|
from .log import logger
|
|
9
9
|
from .model import (
|
|
10
|
+
ReferenceAssignment,
|
|
10
11
|
TestCaseDetails,
|
|
11
12
|
TestCaseSetDetails,
|
|
12
13
|
TestCaseSetNode,
|
|
@@ -94,6 +95,8 @@ class TestBenchJsonReader:
|
|
|
94
95
|
for tcs_uid, tcs in self.test_case_sets.items():
|
|
95
96
|
tc_catalog: dict[str, TestCaseDetails] = {}
|
|
96
97
|
for tc_uid in self.get_test_case_uids(tcs_uid):
|
|
98
|
+
if tc_uid not in self.test_cases:
|
|
99
|
+
continue
|
|
97
100
|
tc_catalog[tc_uid] = self.test_cases[tc_uid]
|
|
98
101
|
tcs_catalog[tcs_uid] = TestCaseSet(tcs, tc_catalog)
|
|
99
102
|
return tcs_catalog
|
|
@@ -132,6 +135,12 @@ class TestBenchJsonReader:
|
|
|
132
135
|
return None
|
|
133
136
|
return from_dict(TestStructureTree, test_structure_tree)
|
|
134
137
|
|
|
138
|
+
def read_references(self) -> list[ReferenceAssignment]:
|
|
139
|
+
references = read_json(str(Path(self.json_dir, "references.json")))
|
|
140
|
+
if references is None:
|
|
141
|
+
return []
|
|
142
|
+
return [from_dict(ReferenceAssignment, ref) for ref in references]
|
|
143
|
+
|
|
135
144
|
|
|
136
145
|
def read_json(filepath: str, silent=True):
|
|
137
146
|
try:
|
|
@@ -7,6 +7,7 @@ from typing import Union
|
|
|
7
7
|
from .config import Configuration
|
|
8
8
|
from .log import logger
|
|
9
9
|
from .model import (
|
|
10
|
+
ReferenceAssignment,
|
|
10
11
|
TestCaseDetails,
|
|
11
12
|
TestCaseSetDetails,
|
|
12
13
|
TestCaseSetExecutionForImport,
|
|
@@ -45,6 +46,17 @@ def write_main_protocol(json_dir: str, main_protocol: list[TestCaseSetExecutionF
|
|
|
45
46
|
)
|
|
46
47
|
|
|
47
48
|
|
|
49
|
+
def write_references(json_dir: str, references: list[ReferenceAssignment]) -> None:
|
|
50
|
+
filepath = Path(json_dir) / Path("references.json")
|
|
51
|
+
with Path(filepath).open("w+", encoding="utf8") as output_file:
|
|
52
|
+
json.dump(
|
|
53
|
+
[asdict(ref) for ref in references],
|
|
54
|
+
output_file,
|
|
55
|
+
indent=2,
|
|
56
|
+
default=lambda o: o.value if isinstance(o, Enum) else str(o),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
48
60
|
def write_default_config(config_file):
|
|
49
61
|
with Path(config_file).open("w+", encoding="utf-8") as file:
|
|
50
62
|
json.dump(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# generated by datamodel-codegen:
|
|
2
2
|
# filename: openapi.yml
|
|
3
|
-
# timestamp: 2025-
|
|
3
|
+
# timestamp: 2025-08-04T13:02:46+00:00
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -828,6 +828,12 @@ class TestCaseDetailsOrigin(Enum):
|
|
|
828
828
|
Upgraded = 'Upgraded'
|
|
829
829
|
|
|
830
830
|
|
|
831
|
+
class ReferenceKind(Enum):
|
|
832
|
+
Reference = 'Reference'
|
|
833
|
+
Link = 'Link'
|
|
834
|
+
Attachment = 'Attachment'
|
|
835
|
+
|
|
836
|
+
|
|
831
837
|
class TestFilterType(Enum):
|
|
832
838
|
TestTheme = 'TestTheme'
|
|
833
839
|
TestCaseSet = 'TestCaseSet'
|
|
@@ -1306,6 +1312,14 @@ class StringUDF(UDF):
|
|
|
1306
1312
|
pass
|
|
1307
1313
|
|
|
1308
1314
|
|
|
1315
|
+
@dataclass
|
|
1316
|
+
class ReferenceAssignment:
|
|
1317
|
+
key: str
|
|
1318
|
+
value: str
|
|
1319
|
+
referenceType: ReferenceKind
|
|
1320
|
+
versionName: Optional[str] = None
|
|
1321
|
+
|
|
1322
|
+
|
|
1309
1323
|
@dataclass
|
|
1310
1324
|
class ParameterSummary:
|
|
1311
1325
|
definitionType: ParameterDefinitionType
|
|
@@ -1324,7 +1338,6 @@ class AssignedDefect:
|
|
|
1324
1338
|
id: str
|
|
1325
1339
|
description: str
|
|
1326
1340
|
identicalVersionKey: str
|
|
1327
|
-
tester: str
|
|
1328
1341
|
status: str
|
|
1329
1342
|
priority: str
|
|
1330
1343
|
classification: str
|
|
@@ -1332,6 +1345,7 @@ class AssignedDefect:
|
|
|
1332
1345
|
references: List[str]
|
|
1333
1346
|
udfs: List[DefectAttribute]
|
|
1334
1347
|
version: Optional[str] = None
|
|
1348
|
+
tester: Optional[str] = None
|
|
1335
1349
|
lastEditTime: Optional[str] = None
|
|
1336
1350
|
lastEditorKey: Optional[str] = None
|
|
1337
1351
|
defectManagementSystem: Optional[str] = None
|
|
@@ -1636,9 +1650,3 @@ class CycleForUpdate:
|
|
|
1636
1650
|
status: Optional[ProjectStatus] = None
|
|
1637
1651
|
testingIntelligence: Optional[bool] = None
|
|
1638
1652
|
startDate: Optional[OptionalLocalDateTime] = None
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
@dataclass
|
|
1642
|
-
class Reference: # TODO: May be changed. Differs to OpenApi.YML
|
|
1643
|
-
type: RepresentativeType
|
|
1644
|
-
path: str
|
|
@@ -7,16 +7,16 @@ import uuid
|
|
|
7
7
|
from datetime import datetime, timedelta, timezone
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from shutil import copytree
|
|
10
|
-
from typing import Optional
|
|
11
|
-
from urllib.parse import unquote
|
|
10
|
+
from typing import Optional
|
|
12
11
|
|
|
13
12
|
from robot.result import Keyword, ResultVisitor, TestCase, TestSuite
|
|
14
13
|
|
|
15
14
|
from testbench2robotframework.model_utils import from_dict
|
|
16
15
|
|
|
17
|
-
from .config import
|
|
16
|
+
from .config import Configuration
|
|
17
|
+
from .execution_artifacts import ExecutionArtifactStorage
|
|
18
18
|
from .json_reader import TestBenchJsonReader
|
|
19
|
-
from .json_writer import write_main_protocol, write_test_structure_element
|
|
19
|
+
from .json_writer import write_main_protocol, write_references, write_test_structure_element
|
|
20
20
|
from .log import logger
|
|
21
21
|
from .model import (
|
|
22
22
|
ActivityStatus,
|
|
@@ -27,8 +27,6 @@ from .model import (
|
|
|
27
27
|
InteractionCallExecution,
|
|
28
28
|
InteractionType,
|
|
29
29
|
InteractionVerdict,
|
|
30
|
-
Reference,
|
|
31
|
-
RepresentativeType,
|
|
32
30
|
RichTextForImport,
|
|
33
31
|
SequencePhase,
|
|
34
32
|
TestCaseDetails,
|
|
@@ -37,7 +35,7 @@ from .model import (
|
|
|
37
35
|
TestCaseSetExecutionForImport,
|
|
38
36
|
VerdictStatus,
|
|
39
37
|
)
|
|
40
|
-
from .utils import directory_to_zip,
|
|
38
|
+
from .utils import directory_to_zip, get_directory
|
|
41
39
|
|
|
42
40
|
try:
|
|
43
41
|
from robot.result import Group
|
|
@@ -62,6 +60,7 @@ COLOR = {
|
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
MEGABYTE = 1000 * 1000
|
|
63
|
+
TB_ARTIFACT_REGEX = r"itb-reference:\s*(\S*)"
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
class ResultWriter(ResultVisitor):
|
|
@@ -92,9 +91,7 @@ class ResultWriter(ResultVisitor):
|
|
|
92
91
|
copytree(self.json_dir, self.json_result, dirs_exist_ok=True)
|
|
93
92
|
self.json_reader = TestBenchJsonReader(self.json_dir)
|
|
94
93
|
self.attachments_path = Path(self.json_result, "attachments")
|
|
95
|
-
|
|
96
|
-
# shutil.rmtree(self.attachments_path)
|
|
97
|
-
# os.mkdir(self.attachments_path)
|
|
94
|
+
self.artifact_storage = self._create_artifact_storage()
|
|
98
95
|
self.test_suites: dict[str, TestSuite] = {}
|
|
99
96
|
self.keywords: list[Keyword] = []
|
|
100
97
|
self.itb_test_case_catalog: dict[str, TestCaseDetails] = {}
|
|
@@ -102,6 +99,15 @@ class ResultWriter(ResultVisitor):
|
|
|
102
99
|
self.test_chain: list[TestCase] = []
|
|
103
100
|
self.main_protocol = from_dict(ExecutionImportingSuccess, {"testCaseSets": []})
|
|
104
101
|
|
|
102
|
+
def _create_artifact_storage(self):
|
|
103
|
+
return ExecutionArtifactStorage(
|
|
104
|
+
self.reference_behaviour,
|
|
105
|
+
self.attachment_conflict_behaviour,
|
|
106
|
+
self.json_reader.read_references(),
|
|
107
|
+
self.output_xml,
|
|
108
|
+
self.attachments_path.as_posix(),
|
|
109
|
+
)
|
|
110
|
+
|
|
105
111
|
def start_suite(self, suite: TestSuite):
|
|
106
112
|
if suite.metadata:
|
|
107
113
|
self.test_suites[suite.metadata["uniqueID"]] = suite
|
|
@@ -114,11 +120,6 @@ class ResultWriter(ResultVisitor):
|
|
|
114
120
|
if interaction.spec.interactionType == interaction_type:
|
|
115
121
|
yield interaction
|
|
116
122
|
|
|
117
|
-
# def _propergate_sequence_phase(self, interaction: InteractionCall, sequence_phase):
|
|
118
|
-
# for child in interaction.interactions:
|
|
119
|
-
# child.spec.sequencePhase = sequence_phase
|
|
120
|
-
# self._propergate_sequence_phase(child, sequence_phase)
|
|
121
|
-
|
|
122
123
|
def end_test(self, test: TestCase):
|
|
123
124
|
self._test_setup_passed = None
|
|
124
125
|
test_chain = get_test_chain(test.name, self.phase_pattern)
|
|
@@ -137,8 +138,6 @@ class ResultWriter(ResultVisitor):
|
|
|
137
138
|
if not itb_test_case:
|
|
138
139
|
logger.warning(f"No JSON file corresponding to test '{test_uid}' found in report.")
|
|
139
140
|
return
|
|
140
|
-
# for interaction in itb_test_case.testSequence:
|
|
141
|
-
# self._propergate_sequence_phase(interaction, interaction.spec.sequencePhase)
|
|
142
141
|
self.protocol_test_case: TestCaseExecutionForImport = TestCaseExecutionForImport(
|
|
143
142
|
test_uid, itb_test_case.exec.key, None, None, None
|
|
144
143
|
)
|
|
@@ -163,8 +162,7 @@ class ResultWriter(ResultVisitor):
|
|
|
163
162
|
)
|
|
164
163
|
self._set_itb_testcase_execution_result(itb_test_case, self.test_chain)
|
|
165
164
|
self._set_itb_testcase_execution_comment(itb_test_case, self.test_chain)
|
|
166
|
-
|
|
167
|
-
self._set_itb_testcase_references(itb_test_case, self.test_chain)
|
|
165
|
+
self._set_itb_testcase_references(itb_test_case, self.test_chain)
|
|
168
166
|
except TypeError as e:
|
|
169
167
|
logger.error(
|
|
170
168
|
"Could not find an itb testcase that corresponds "
|
|
@@ -185,78 +183,14 @@ class ResultWriter(ResultVisitor):
|
|
|
185
183
|
if not itb_test_case.exec:
|
|
186
184
|
return
|
|
187
185
|
for test in test_chain:
|
|
188
|
-
|
|
189
|
-
for
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
def _get_itb_reference(self, test_message: str) -> list[Reference]:
|
|
194
|
-
references = []
|
|
195
|
-
for path in re.findall(r"itb-reference:\s*(\S*)", test_message):
|
|
196
|
-
if path.startswith("file:///"):
|
|
197
|
-
file_path = Path(unquote(path[len("file:///") :]))
|
|
198
|
-
output_dir = Path(self.output_xml).parent
|
|
199
|
-
if file_path.exists():
|
|
200
|
-
reference_path = file_path
|
|
201
|
-
elif Path(output_dir, file_path).exists():
|
|
202
|
-
reference_path = Path(output_dir, file_path)
|
|
203
|
-
else:
|
|
204
|
-
if (
|
|
205
|
-
file_path.is_absolute()
|
|
206
|
-
and self.reference_behaviour == ReferenceBehaviour.REFERENCE
|
|
207
|
-
):
|
|
208
|
-
references.append(self._create_reference(file_path))
|
|
209
|
-
else:
|
|
210
|
-
logger.warning(f"Referenced file '{file_path}' does not exist.")
|
|
211
|
-
continue
|
|
212
|
-
file_size = Path.stat(reference_path).st_size
|
|
213
|
-
reference: Optional[Reference] = None
|
|
214
|
-
if file_size >= 10 * MEGABYTE:
|
|
215
|
-
logger.error(
|
|
216
|
-
f"Trying to attach file '{reference_path}'. "
|
|
217
|
-
"Attachment file size must not exceed 10 MB "
|
|
218
|
-
f"but is {file_size / MEGABYTE} MB."
|
|
219
|
-
)
|
|
220
|
-
reference = self._create_reference(reference_path.name)
|
|
221
|
-
else:
|
|
222
|
-
reference = self._create_attachment(reference_path)
|
|
223
|
-
if reference:
|
|
224
|
-
references.append(reference)
|
|
225
|
-
elif re.match(r"\S+://", path):
|
|
226
|
-
references.append(Reference(RepresentativeType.Hyperlink, path))
|
|
227
|
-
else:
|
|
228
|
-
logger.warning(f"Could not identify type of test message reference '{path}'.")
|
|
229
|
-
return references
|
|
186
|
+
reference_values = self._get_itb_reference_values(test.message)
|
|
187
|
+
for reference_value in reference_values:
|
|
188
|
+
reference_key = self.artifact_storage.add_artifact(reference_value)
|
|
189
|
+
if reference_key and reference_key not in itb_test_case.exec.references:
|
|
190
|
+
itb_test_case.exec.references.append(reference_key)
|
|
230
191
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return Reference(RepresentativeType.Reference, str(reference_path))
|
|
234
|
-
|
|
235
|
-
def _create_attachment(self, filepath: Path) -> Optional[Reference]:
|
|
236
|
-
if self.reference_behaviour == ReferenceBehaviour.REFERENCE:
|
|
237
|
-
return self._create_reference(filepath.resolve())
|
|
238
|
-
ensure_dir_exists(self.attachments_path)
|
|
239
|
-
filename = Path(filepath).name
|
|
240
|
-
if (
|
|
241
|
-
not Path(self.attachments_path, filename).exists()
|
|
242
|
-
or self.attachment_conflict_behaviour == AttachmentConflictBehaviour.USE_NEW
|
|
243
|
-
):
|
|
244
|
-
shutil.copyfile(filepath, self.attachments_path / filename, follow_symlinks=True)
|
|
245
|
-
return Reference(RepresentativeType.Attachment, f"attachments/{filename}")
|
|
246
|
-
if self.attachment_conflict_behaviour == AttachmentConflictBehaviour.USE_EXISTING:
|
|
247
|
-
return Reference(RepresentativeType.Attachment, f"attachments/{filename}")
|
|
248
|
-
if self.attachment_conflict_behaviour == AttachmentConflictBehaviour.RENAME_NEW:
|
|
249
|
-
unique_path = self._create_unique_path(self.attachments_path / filename)
|
|
250
|
-
shutil.copyfile(filepath, unique_path, follow_symlinks=True)
|
|
251
|
-
unique_file = Path(unique_path).name
|
|
252
|
-
logger.info(
|
|
253
|
-
f"Attachment '{filename}' does already exist. "
|
|
254
|
-
f"Creating new unique attachment '{unique_file}'."
|
|
255
|
-
)
|
|
256
|
-
return Reference(RepresentativeType.Attachment, f"attachments/{unique_file}")
|
|
257
|
-
if self.attachment_conflict_behaviour == AttachmentConflictBehaviour.ERROR:
|
|
258
|
-
logger.error(f"Attachment '{filename}' does already exist.")
|
|
259
|
-
return None
|
|
192
|
+
def _get_itb_reference_values(self, test_message: str) -> list[str]:
|
|
193
|
+
return re.findall(f".*{TB_ARTIFACT_REGEX}.*", test_message)
|
|
260
194
|
|
|
261
195
|
@staticmethod
|
|
262
196
|
def _create_unique_path(attachement_path: Path) -> Path:
|
|
@@ -273,7 +207,7 @@ class ResultWriter(ResultVisitor):
|
|
|
273
207
|
def _set_itb_testcase_execution_comment(self, itb_test_case, test_chain: list[TestCase]):
|
|
274
208
|
exec_comments = []
|
|
275
209
|
for test in test_chain:
|
|
276
|
-
message = re.sub(
|
|
210
|
+
message = re.sub(TB_ARTIFACT_REGEX, "", test.message)
|
|
277
211
|
html_message = (
|
|
278
212
|
message[len("*HTML*") :].replace("<hr>", "<br/>").replace("<br>", "<br/>").strip()
|
|
279
213
|
if test.message.startswith("*HTML*")
|
|
@@ -597,7 +531,7 @@ class ResultWriter(ResultVisitor):
|
|
|
597
531
|
name = test.name
|
|
598
532
|
phase = ""
|
|
599
533
|
if test.status != "PASS":
|
|
600
|
-
message = re.sub(
|
|
534
|
+
message = re.sub(TB_ARTIFACT_REGEX, "", test.message)
|
|
601
535
|
message = (
|
|
602
536
|
message[len("*HTML*") :]
|
|
603
537
|
.replace("<hr>", "<br />")
|
|
@@ -685,6 +619,7 @@ class ResultWriter(ResultVisitor):
|
|
|
685
619
|
test_suite_counter += 1
|
|
686
620
|
write_test_structure_element(self.json_result, tt_tree)
|
|
687
621
|
write_main_protocol(self.json_result, self.main_protocol.testCaseSets)
|
|
622
|
+
write_references(self.json_result, self.artifact_storage.tb_references)
|
|
688
623
|
if test_suite_counter and self.itb_test_case_catalog:
|
|
689
624
|
logger.info(f"Successfully read {test_suite_counter} test suites.")
|
|
690
625
|
else:
|
|
@@ -125,6 +125,17 @@ class RfTestCase:
|
|
|
125
125
|
def _get_interaction_call(self, test_step: InteractionCall) -> None:
|
|
126
126
|
indent = len(test_step.numbering.split("."))
|
|
127
127
|
if test_step.spec.interactionType == InteractionType.Textual:
|
|
128
|
+
self.rf_interaction_calls.append(
|
|
129
|
+
RFInteractionCall(
|
|
130
|
+
name=f"# {test_step.spec.name}",
|
|
131
|
+
cbv_parameters={},
|
|
132
|
+
cbr_parameters={},
|
|
133
|
+
indent=indent,
|
|
134
|
+
import_prefix=None,
|
|
135
|
+
sequence_phase=test_step.spec.sequencePhase,
|
|
136
|
+
is_atomic=True,
|
|
137
|
+
)
|
|
138
|
+
)
|
|
128
139
|
return
|
|
129
140
|
cbv_params = self._get_params_by_use_type(test_step, ParameterEvaluationType.CallByValue)
|
|
130
141
|
cbr_params = self._get_params_by_use_type(
|
|
@@ -180,22 +191,19 @@ class RfTestCase:
|
|
|
180
191
|
for pattern in self.lib_pattern_list:
|
|
181
192
|
match = pattern.search(interaction_path)
|
|
182
193
|
if match:
|
|
183
|
-
return LIBRARY_IMPORT_TYPE, match
|
|
194
|
+
return LIBRARY_IMPORT_TYPE, match.group("resourceName").strip()
|
|
184
195
|
for pattern in self.res_pattern_list:
|
|
185
196
|
match = pattern.search(interaction_path)
|
|
186
197
|
if match:
|
|
187
|
-
return RESOURCE_IMPORT_TYPE,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return LIBRARY_IMPORT_TYPE,
|
|
195
|
-
|
|
196
|
-
return RESOURCE_IMPORT_TYPE, import_prefix
|
|
197
|
-
|
|
198
|
-
return root_subdivision, import_prefix
|
|
198
|
+
return RESOURCE_IMPORT_TYPE, interaction_path
|
|
199
|
+
splitted_interaction_path = interaction_path.split(".")
|
|
200
|
+
minimum_length_subdivision_path_length = 2
|
|
201
|
+
if (
|
|
202
|
+
len(splitted_interaction_path) == minimum_length_subdivision_path_length
|
|
203
|
+
and splitted_interaction_path[0] in self.config.library_root
|
|
204
|
+
):
|
|
205
|
+
return LIBRARY_IMPORT_TYPE, splitted_interaction_path[1]
|
|
206
|
+
return UNKNOWN_IMPORT_TYPE, interaction_path
|
|
199
207
|
|
|
200
208
|
def _append_compound_ia(
|
|
201
209
|
self,
|
|
@@ -436,6 +444,11 @@ class RfTestCase:
|
|
|
436
444
|
filter(lambda parameter: parameter != "", interaction.cbr_parameters.values())
|
|
437
445
|
)
|
|
438
446
|
for index, parameter in enumerate(cbr_parameters):
|
|
447
|
+
if not parameter:
|
|
448
|
+
logger.warning(
|
|
449
|
+
f"Interaction {interaction.name} has undefined CallByReference parameter value."
|
|
450
|
+
)
|
|
451
|
+
continue
|
|
439
452
|
if not parameter.startswith("${"):
|
|
440
453
|
cbr_parameters[index] = f"${{{parameter}}}"
|
|
441
454
|
return cbr_parameters
|
|
@@ -622,6 +635,7 @@ class RobotSuiteFileBuilder:
|
|
|
622
635
|
test_case_section = TestCaseSection(header=SectionHeader.from_params(Token.TESTCASE_HEADER))
|
|
623
636
|
robot_ast_test_cases = []
|
|
624
637
|
for index, test_case in enumerate(self._rf_test_cases):
|
|
638
|
+
logger.debug(f"Processing test case: {test_case.uid}")
|
|
625
639
|
robot_ast_test_cases.extend(test_case.to_robot_ast_test_cases())
|
|
626
640
|
if index != len(self._rf_test_cases) - 1:
|
|
627
641
|
robot_ast_test_cases[-1].body.extend(LINE_SEPARATOR)
|
|
@@ -669,36 +683,53 @@ class RobotSuiteFileBuilder:
|
|
|
669
683
|
} # TODO Fix Paths to correct models
|
|
670
684
|
return [ResourceImport.from_params(res) for res in sorted(resource_paths)]
|
|
671
685
|
|
|
672
|
-
def
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if
|
|
677
|
-
return
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
self.config.resource_directory
|
|
686
|
-
)
|
|
687
|
-
resource_import = (
|
|
688
|
-
Path(os.path.relpath(Path(resource_directory), robot_file_path))
|
|
689
|
-
/ f"{resource}.resource"
|
|
686
|
+
def _get_resource_name(self, resource: str) -> str | None:
|
|
687
|
+
resource_path_part = resource.split(".")[-1]
|
|
688
|
+
for resource_regex in self.config.resource_regex:
|
|
689
|
+
resource_name_match = re.search(resource_regex, resource_path_part, flags=re.IGNORECASE)
|
|
690
|
+
if resource_name_match:
|
|
691
|
+
return resource_name_match.group("resourceName").strip()
|
|
692
|
+
return None
|
|
693
|
+
|
|
694
|
+
def _get_resource_directory_path_index(self, resource: str) -> int | None:
|
|
695
|
+
splitted_interaction_path = resource.split(".")
|
|
696
|
+
for index, part in enumerate(splitted_interaction_path):
|
|
697
|
+
resource_directory_match = re.match(
|
|
698
|
+
self.config.resource_directory_regex, part, flags=re.IGNORECASE
|
|
690
699
|
)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
if resource_directory_match:
|
|
701
|
+
return index
|
|
702
|
+
return None
|
|
703
|
+
|
|
704
|
+
def _get_resource_path_index(self, resource: str) -> int | None:
|
|
705
|
+
return len(resource.split(".")) - 1 if resource else None
|
|
706
|
+
|
|
707
|
+
def _create_resource_path(self, resource: str) -> str:
|
|
708
|
+
splitted_interaction_path = resource.split(".")
|
|
709
|
+
resource_dir_index = self._get_resource_directory_path_index(resource)
|
|
710
|
+
resource_name = self._get_resource_name(resource)
|
|
711
|
+
resource_name_index = self._get_resource_path_index(resource)
|
|
712
|
+
cropped_interaction_path = []
|
|
713
|
+
if resource_dir_index is None:
|
|
714
|
+
return f"{resource_name}.resource"
|
|
715
|
+
cropped_interaction_path.extend(
|
|
716
|
+
splitted_interaction_path[resource_dir_index + 1 : resource_name_index]
|
|
717
|
+
)
|
|
718
|
+
resource_path = Path(
|
|
719
|
+
self.config.resource_directory,
|
|
720
|
+
*cropped_interaction_path,
|
|
721
|
+
f"{resource_name}.resource",
|
|
722
|
+
).as_posix()
|
|
723
|
+
resource_path = self.config.subdivisionsMapping.resources.get(resource_name, resource_path)
|
|
724
|
+
resource_path = re.sub(
|
|
725
|
+
r"^{resourceDirectory}", self.config.resource_directory, resource_path
|
|
695
726
|
)
|
|
696
|
-
|
|
727
|
+
root_path = Path(os.curdir).absolute()
|
|
728
|
+
return re.sub(
|
|
697
729
|
RELATIVE_RESOURCE_INDICATOR,
|
|
698
730
|
str(root_path).replace("\\", "/"),
|
|
699
|
-
|
|
731
|
+
resource_path,
|
|
700
732
|
)
|
|
701
|
-
return str(subdivision_mapping)
|
|
702
733
|
|
|
703
734
|
def _replace_relative_resource_indicator(self, path: Path | str) -> str:
|
|
704
735
|
root_path = Path(os.curdir).absolute()
|
|
@@ -4,6 +4,7 @@ import sys
|
|
|
4
4
|
from pathlib import Path, PurePath
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from zipfile import ZipFile
|
|
7
|
+
from .log import logger
|
|
7
8
|
|
|
8
9
|
from testbench2robotframework.model import (
|
|
9
10
|
RootNode,
|
|
@@ -45,6 +46,9 @@ class PathResolver:
|
|
|
45
46
|
self.tt_paths = self._get_paths(self.tt_catalog)
|
|
46
47
|
|
|
47
48
|
def _analyze_tree(self, test_theme_tree: TestStructureTree):
|
|
49
|
+
if not test_theme_tree.root:
|
|
50
|
+
logger.warning("Test Structure Tree contains no root node.")
|
|
51
|
+
return
|
|
48
52
|
self.tree_dict[test_theme_tree.root.base.key] = test_theme_tree.root
|
|
49
53
|
self._add_existing_tcs_to_catalog(test_theme_tree.root)
|
|
50
54
|
for tse in test_theme_tree.nodes:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/LibrarySubdivision.PNG
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_missing_files.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|