test-specification: Add support for python f-strings in step and description

This is only availible for tests that require paramters. If the test have a
parameter --data the description and test steps can contain {data} which will
be replaced with tha actual data of the parameter data in the test specification.
This commit is contained in:
Mattias Walström
2025-01-14 10:54:05 +01:00
parent 1aab75d86a
commit 9a831217e4
+42 -4
View File
@@ -67,15 +67,42 @@ class TestStepVisitor(ast.NodeVisitor):
self.description=self.description.strip()
self.generic_visit(node)
def _rebuild_fstring(self, node: ast.JoinedStr) -> str:
"""
Rebuild an f-string (JoinedStr) by concatenating literal pieces and placeholders.
"""
parts = []
for value in node.values:
if isinstance(value, ast.Constant):
parts.append(value.value)
elif isinstance(value, ast.FormattedValue):
expression = ast.unparse(value.value) if hasattr(ast, "unparse") else ast.dump(value.value)
parts.append(f"{{{expression}}}")
return "".join(parts)
# Check for test.step() for the actual test steps
def visit_Call(self, node):
"""Extract test.step from test"""
if isinstance(node.func, ast.Attribute) and node.func.attr == 'step':
arg = node.args[0]
if isinstance(node.func.value, ast.Name) and node.func.value.id == 'test':
if node.args and isinstance(node.args[0], ast.Constant):
self.test_steps.append(node.args[0].value)
if node.args:
if isinstance(arg, ast.Constant):
self.test_steps.append(node.args[0].value)
elif isinstance(arg, ast.JoinedStr):
fstring_value = self._rebuild_fstring(arg)
self.test_steps.append(fstring_value)
self.generic_visit(node) # Continue visiting other nodes
def replace_in_steps(steps, variables):
result = []
for step in steps:
new_step = step
for k, v in variables.items():
new_step = new_step.replace("{" + k + "}", v)
result.append(new_step)
return result
class TestCase:
"""All test specifcation resources for a test case"""
def __init__(self, directory, rootdir=None):
@@ -103,7 +130,7 @@ class TestCase:
f.write(mod_content)
def generate_specification(self, name, case_path, spec_path):
def generate_specification(self, name, case_path, spec_path, variables):
"""Generate a ASCIIDOC specification for the test case"""
with open(case_path, 'r') as file:
script_content = file.read()
@@ -113,6 +140,12 @@ class TestCase:
visitor.visit(parsed_script)
description=visitor.description if visitor.description != "" else "Undefined"
test_steps=visitor.test_steps
if variables is not None:
for k,v in variables.items():
if "{" + k + "}" in description:
description = description.replace("{" + k + "}", v)
test_steps=replace_in_steps(test_steps, variables)
if name is None:
name = visitor.name
self.generate_topology()
@@ -150,6 +183,7 @@ def parse_suite(directory, root, suitefile):
path = os.path.dirname(entry["suite"])
parse_suite(f"{directory}/{path}", root, f"{directory}/{entry['suite']}")
elif entry.get("case"):
variables=None
if not entry.get("infamy"):
test_title = None # Set from the docstring in the test case
else:
@@ -161,10 +195,14 @@ def parse_suite(directory, root, suitefile):
if readme is None:
readme = open(f"{directory}/{path}/Readme.adoc.tmp", "w")
test_case_id = entry["name"]
if "opts" in entry:
variables = dict((k[2:], v) for k, v in zip(entry["opts"][::2], entry["opts"][1::2]))
test_file = f"{directory}/{entry['case']}"
test_spec = f"{directory}/{path}/{test_case_id}.adoc"
test_case = TestCase(f"{directory}/{path}", root)
test_case.generate_specification(test_title, test_file, test_spec)
test_case.generate_specification(test_title, test_file, test_spec, variables)
readme.write(f"include::{path}{test_case_id}.adoc[]\n\n")
if path != "":
readme.close()