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.
Files changed (58) hide show
  1. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/.gitignore +8 -1
  2. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/PKG-INFO +1 -1
  3. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/__init__.py +1 -1
  4. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/cli.py +37 -16
  5. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/config.py +23 -3
  6. testbench2robotframework-0.8.0a5/testbench2robotframework/execution_artifacts.py +245 -0
  7. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/json_reader.py +9 -0
  8. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/json_writer.py +12 -0
  9. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/model.py +16 -8
  10. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/result_writer.py +27 -92
  11. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testbench2rf.py +69 -38
  12. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/utils.py +4 -0
  13. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/CreatePiPWheel.bat +0 -0
  14. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/CreatePiPWheel.sh +0 -0
  15. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/DEVELOPMENT.md +0 -0
  16. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/json_config.json +0 -0
  17. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/pyproject_example.toml +0 -0
  18. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/ExampleConfiguration/toml_config.toml +0 -0
  19. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/LICENSE +0 -0
  20. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/MANIFEST.in +0 -0
  21. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/README.md +0 -0
  22. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/json_config_tests/1_tfs.robot +0 -0
  23. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/libs/json_config.py +0 -0
  24. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/libs/pyproject_config.py +0 -0
  25. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/resources/file_management.resource +0 -0
  26. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/resources/testbench2robotframework_cli.resource +0 -0
  27. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/json_config.robot +0 -0
  28. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/no_config_argument.robot +0 -0
  29. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/atest/robot/rf_tests/cli_interface/write/toml_config.robot +0 -0
  30. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/create_json_schema.py +0 -0
  31. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/LibrarySubdivision.PNG +0 -0
  32. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/Unbenannt.PNG +0 -0
  33. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/generated.png +0 -0
  34. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/libraries.PNG +0 -0
  35. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/resources.PNG +0 -0
  36. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/rfLibraryRootsTestBench.PNG +0 -0
  37. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testbench_rfLibraryRegex.PNG +0 -0
  38. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testbench_rfResourceRegex.PNG +0 -0
  39. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/images/testthemen.PNG +0 -0
  40. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/oldModel.py +0 -0
  41. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/pydantic_model.py +0 -0
  42. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/pyproject.toml +0 -0
  43. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/requirements.txt +0 -0
  44. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/robot.toml +0 -0
  45. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tasks.py +0 -0
  46. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench-tools.zip +0 -0
  47. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/__main__.py +0 -0
  48. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/html_parser.py +0 -0
  49. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/log.py +0 -0
  50. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/model_utils.py +0 -0
  51. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/robotframework2testbench.py +0 -0
  52. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testbench2robotframework.py +0 -0
  53. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/testbench2robotframework/testsuite_write.py +0 -0
  54. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_data/configurations/invalid_config.json +0 -0
  55. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_data/configurations/valid_config.json +0 -0
  56. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_missing_files.py +0 -0
  57. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_robot_files_should_not_contain_invalid_characters.py +0 -0
  58. {testbench2robotframework-0.8.0a3 → testbench2robotframework-0.8.0a5}/tests/test_zip_file_generation.py +0 -0
@@ -157,4 +157,11 @@ README_new.md
157
157
  report/
158
158
  reportt/
159
159
  itorx_report/
160
- config.json
160
+ config.json
161
+ resources/
162
+ robot.toml
163
+ .robot.toml
164
+ cli-export-config.json
165
+ cli-import-config.json
166
+ robot_tests/
167
+ reports/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: testbench2robotframework
3
- Version: 0.8.0a3
3
+ Version: 0.8.0a5
4
4
  Summary: Robot Framework Code Generator from Keyword-Driven Tests in imbus TestBench 3.0 and newer
5
5
  Author-email: imbus AG <support@imbus.de>
6
6
  Requires-Python: >= 3.10
@@ -17,4 +17,4 @@ suites and enhancing the TestBench report with execution results from Robot Fram
17
17
 
18
18
  from .testbench2robotframework import testbench2robotframework # noqa: F401
19
19
 
20
- __version__ = "0.8.0a3"
20
+ __version__ = "0.8.0a5"
@@ -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="""Option to call Robot Framework keywords by their
76
- fully qualified name in the generated test suites.""",
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="""Regex that can be used to identify TestBench
102
- Subdivisions that correspond to Robot Framework libraries.""",
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="""Regex that can be used to identify TestBench Subdivisions
116
- that correspond to Robot Framework resources.""",
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
- config_path = config or pyproject_toml
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
- configuration = {}
222
- elif config_path.suffix == ".json":
223
- configuration = read_json(config, False)
224
- else:
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("testCaseSplitPathRegEx", ".*StopWithRestart.*"),
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("referenceBehaviour", "ATTACHMENT").upper()
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("attachmentConflictBehaviour", "USE_EXISTING").upper()
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-06-23T15:17:05+00:00
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, Union
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 AttachmentConflictBehaviour, Configuration, ReferenceBehaviour
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, ensure_dir_exists, get_directory
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
- # if self.attachments_path.exists(): TODO: RR Sollten wir löschen????
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
- if self.reference_behaviour != ReferenceBehaviour.NONE:
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
- itb_references = self._get_itb_reference(test.message)
189
- for reference in itb_references:
190
- if reference not in itb_test_case.exec.references:
191
- itb_test_case.exec.references.append(reference)
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
- @staticmethod
232
- def _create_reference(reference_path: Union[Path, str]) -> Reference:
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(r"\s*itb-reference:\s*(\S*)", "", test.message)
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(r"\s*itb-reference:\s*(\S*)", "", test.message)
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[1].strip()
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, match[1].strip()
188
-
189
- ia_path_parts = interaction_path.split(".")
190
- if len(ia_path_parts) == 1:
191
- return UNKNOWN_IMPORT_TYPE, ia_path_parts[0]
192
- root_subdivision, import_prefix = ia_path_parts[:2]
193
- if root_subdivision in self.config.library_root:
194
- return LIBRARY_IMPORT_TYPE, import_prefix
195
- if root_subdivision in self.config.resource_root:
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 _create_resource_path(self, resource: str) -> str:
673
- subdivision_mapping = self.config.subdivisionsMapping.resources.get(resource)
674
- resource = re.sub(".resource", "", resource)
675
- if not subdivision_mapping:
676
- if not self.config.resource_directory:
677
- return f"{resource}.resource"
678
- if not re.match(RELATIVE_RESOURCE_INDICATOR, self.config.resource_directory):
679
- return Path(self.config.resource_directory, f"{resource}.resource").as_posix()
680
- generation_directory = self._replace_relative_resource_indicator(
681
- self.config.output_directory
682
- )
683
- robot_file_path = Path(generation_directory) / self.tcs_path.parent
684
- resource_directory = self._replace_relative_resource_indicator(
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
- return resource_import.as_posix()
692
- root_path = Path(os.curdir).absolute()
693
- subdivision_mapping = re.sub(
694
- r"^{resourceDirectory}", self.config.resource_directory, subdivision_mapping
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
- subdivision_mapping = re.sub(
727
+ root_path = Path(os.curdir).absolute()
728
+ return re.sub(
697
729
  RELATIVE_RESOURCE_INDICATOR,
698
730
  str(root_path).replace("\\", "/"),
699
- subdivision_mapping,
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: