Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion testbench2robotframework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@

from .testbench2robotframework import testbench2robotframework # noqa: F401

__version__ = "0.8.1a1"
__version__ = "0.9.0a1"
12 changes: 11 additions & 1 deletion testbench2robotframework/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,15 @@ def testbench2robotframework_cli():
help="""TestBench root subdivision which's direct
children correspond to Robot Framework libraries.""",
)
@click.option(
"--metadata",
multiple=True,
callback=parse_subdivision_mapping,
help="""Add extra metadata to the settings of the generated Robot Framework test suite.
Provide entries as key:value pairs, where *key* is the metadata name and *value* is the corresponding value.
Values may also be Python expressions.
The special variable '$tcs' gives access to the TestBench Python model of the test case set.""",
)
@click.option(
"--resource-regex",
multiple=True,
Expand Down Expand Up @@ -157,6 +166,7 @@ def generate_tests( # noqa: PLR0913
resource_directory_regex: str,
library_root: tuple[str],
log_suite_numbering: bool,
metadata: dict[str, str],
output_directory: Path,
resource_directory: Path,
resource_regex: tuple[str],
Expand Down Expand Up @@ -187,7 +197,7 @@ def generate_tests( # noqa: PLR0913
configuration["log-suite-numbering"] = True
else:
configuration["log-suite-numbering"] = configuration.get("log-suite-numbering", False)

configuration["metadata"] = metadata or configuration.get("metadata", {})
configuration["compound-interaction-logging"] = (
compound_interaction_logging or configuration.get("compound-interaction-logging", "GROUP")
)
Expand Down
2 changes: 2 additions & 0 deletions testbench2robotframework/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ class Configuration:
library_root: list[str]
log_suite_numbering: bool
loggingConfiguration: LoggingConfig
metadata: dict[str, str]
output_directory: str
phasePattern: str
referenceBehaviour: ReferenceBehaviour
Expand Down Expand Up @@ -227,6 +228,7 @@ def from_dict(cls, dictionary) -> Configuration:
"file":dictionary.get("file-logging", {})
}
),
metadata=dictionary.get("metadata", {}),
compound_interaction_logging=CompoundInteractionLogging(dictionary.get("compound-interaction-logging", "GROUP").upper()),
resource_directory=dictionary.get("resource-directory", "").replace(
"\\", "/"
Expand Down
32 changes: 32 additions & 0 deletions testbench2robotframework/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1651,3 +1651,35 @@ class CycleForUpdate:
status: Optional[ProjectStatus] = None
testingIntelligence: Optional[bool] = None
startDate: Optional[OptionalLocalDateTime] = None



@dataclass
class AllModels:
ProjectMember: ProjectMember
ProjectDetails: ProjectDetails
TOVDetails: TOVDetails
CycleDetails: CycleDetails
UserDetails: UserDetails
TestStructureTree: TestStructureTree
TestCaseDetails: TestCaseDetails
InteractionDetails: InteractionDetails
ParameterSummary: ParameterSummary
TestCaseSpecificationDetails: TestCaseSpecificationDetails
TestCaseExecutionDetails: TestCaseExecutionDetails
TestStructureSpecification: TestStructureSpecification
TestStructureAutomation: TestStructureAutomation
TestStructureExecution: TestStructureExecution
AttachedFilter: AttachedFilter
TestStructureTreeNode: TestStructureTreeNode
TestCaseExecutionSummary: TestCaseExecutionSummary
TestCaseSetExecutionSummary: TestCaseSetExecutionSummary
ActivityStatus: ActivityStatus
ExecStatus: ExecStatus
VerdictStatus: VerdictStatus
SpecStatus: SpecStatus
DataTypeSummary: DataTypeSummary
TestCaseSetDetails: TestCaseSetDetails
TestCaseSetSpecificationSummary: TestCaseSetSpecificationSummary
TestCaseSpecificationSummary: TestCaseSpecificationSummary
TestCaseSummary: TestCaseSummary
47 changes: 36 additions & 11 deletions testbench2robotframework/testbench2rf.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,9 +455,15 @@ def _create_cbr_parameters(

def _get_interaction_import_prefix(self, interaction: RFInteractionCall) -> str:
for resource_regex in self.config.resource_regex:
resource_name_match = re.search(resource_regex, interaction.import_prefix, flags=re.IGNORECASE)
if not interaction.import_prefix:
continue
resource_name_match = re.search(
resource_regex, interaction.import_prefix, flags=re.IGNORECASE
)
if resource_name_match:
return (self.config.fully_qualified or False) * f"{resource_name_match.group('resourceName').strip()}."
return (
self.config.fully_qualified or False
) * f"{resource_name_match.group('resourceName').strip()}."
return ""

def _get_interaction_indent(self, interaction: RFInteractionCall) -> str:
Expand Down Expand Up @@ -715,15 +721,19 @@ def _create_resource_path(self, resource: str) -> str:
resource_name_index = self._get_resource_path_index(resource)
cropped_interaction_path = []
if resource_dir_index is None:
return f"{resource_name}.resource"
cropped_interaction_path.extend(
splitted_interaction_path[resource_dir_index + 1 : resource_name_index]
)
resource_path = Path(
self.config.resource_directory,
*cropped_interaction_path,
f"{resource_name}.resource",
).as_posix()
resource_path = Path(
self.config.resource_directory,
f"{resource_name}.resource",
).as_posix()
else:
cropped_interaction_path.extend(
splitted_interaction_path[resource_dir_index + 1 : resource_name_index]
)
resource_path = Path(
self.config.resource_directory,
*cropped_interaction_path,
f"{resource_name}.resource",
).as_posix()
resource_path = self.config.subdivisionsMapping.resources.get(resource_name, resource_path)
resource_path = re.sub(
r"^{resourceDirectory}", self.config.resource_directory, resource_path
Expand Down Expand Up @@ -815,6 +825,21 @@ def _create_setting_section(self) -> SettingSection:
setting_section.body.extend(self._create_rf_resource_imports(subdivisions))
setting_section.body.extend(self._create_rf_unknown_imports(subdivisions))
setting_section_meta_data = self.test_case_set.metadata
for md_name, md_expression in self.config.metadata.items():
from .utils import safe_eval
md_expression = re.sub(r"\$tcs", "tcs", md_expression)
try:
setting_section_meta_data[md_name] = safe_eval(
md_expression, {"tcs": self.test_case_set.details}
)
except ValueError as ve:
logger.warning(
f"Value '{md_expression}' from the custom metadata setting could not be evaluated: {ve}"
)
except Exception as e:
logger.error(
f"Error while evaluating the custom metadata setting '{md_expression}': {e}"
)
setting_section.body.extend(
[
create_meta_data(metadata_name, metadata_value)
Expand Down
3 changes: 2 additions & 1 deletion testbench2robotframework/testbench2robotframework.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def testbench2robotframework(testbench_report: str, config: dict):
setup_logger(configuration)
logger.debug("Configuration loaded.")
testbench_report = Path(testbench_report)
temp_dir = None
try:
if is_zip_file(testbench_report):
temp_dir = tempfile.TemporaryDirectory(dir=Path.cwd())
Expand All @@ -41,6 +42,6 @@ def testbench2robotframework(testbench_report: str, config: dict):
return
write_test_suites(test_suites, configuration)
except Exception as exception:
if temp_dir:
if temp_dir is not None:
temp_dir.cleanup()
raise exception
52 changes: 52 additions & 0 deletions testbench2robotframework/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ast
import re
import shutil
import sys
Expand Down Expand Up @@ -96,6 +97,57 @@ def _get_padded_index(self, tse) -> str:
return index.zfill(max_length)


def safe_eval(expr: str, names: dict):
tree = ast.parse(expr, mode="eval")
allowed_nodes = (
ast.Expression,
ast.Call,
ast.Attribute,
ast.Load,
ast.Name,
ast.ListComp,
ast.GeneratorExp,
ast.comprehension,
ast.List,
ast.Tuple,
ast.Constant,
ast.Subscript,
ast.JoinedStr,
ast.FormattedValue,
ast.BinOp,
ast.UnaryOp,
ast.Compare,
ast.BoolOp,
ast.Eq,
ast.NotEq,
ast.Gt,
ast.Lt,
ast.GtE,
ast.LtE,
ast.In,
ast.And,
ast.Or,
ast.Not,
ast.Add,
ast.Sub,
ast.Mult,
ast.Div,
ast.Mod,
ast.Store,
ast.keyword,
)
for node in ast.walk(tree):
if not isinstance(node, allowed_nodes):
raise ValueError(f"Disallowed expression: {type(node).__name__}")
if isinstance(node, ast.Attribute) and node.attr.startswith("_"):
raise ValueError(f"Access to private attribute {node.attr} is not allowed")
if isinstance(node, ast.Name) and node.id == "__import__":
raise ValueError("Use of __import__ is forbidden")

code = compile(tree, "<safe_eval>", "eval")
return eval(code, {"__builtins__": {}}, names)


def get_directory(json_report_path: Optional[str]) -> str:
if json_report_path is None:
return ""
Expand Down