Skip to content

Templated dts e2e tests #863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
Draft
205 changes: 205 additions & 0 deletions dts/PlatformParser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from collections.abc import Callable
from pathlib import Path
from typing import Any, TypeAlias

from robot.api.deco import keyword, library
from robot.api.exceptions import Error
from robot.libraries.BuiltIn import BuiltIn
from robot.model.testsuite import TestSuite
from robot.running.namespace import IMPORTER, Namespace
from robot.running.resourcemodel import ResourceFile
from robot.utils.robottypes import is_truthy
from robot.variables.resolvable import Resolvable
from robot.variables.variables import Variables

PlatformVariableName: TypeAlias = str
PlatformVariableValue: TypeAlias = Any
PlatformVariables: TypeAlias = dict[PlatformVariableName, PlatformVariableValue]
PlatformName: TypeAlias = str


@library(scope="TEST", version="5.0")
class PlatformParser:
# Used with 'Get DTS' keywords
SKIP_PLATFORMS = [
"qemu", # requires /tmp/qmp-socket file to exist
"qemu-selftests", # requires /tmp/qmp-socket file to exist
"no-rte", # not a platform?
"novacustom-ts1", # requires INSTALLED_DUT variable to be set
]

@keyword("Get DTS Test Platform Names")
def get_dts_test_platform_names(self) -> list[PlatformName]:
"""Returns list of platforms (filename without extension) that support
DTS testing. Checks all robot files in 'platform-configs' except ones
defined in SKIP_PLATFORMS in this class

Returns:
list[str]: list of platforms that support DTS testing
"""
return [platform for platform, _ in self._get_dts_test_platforms()]

@keyword("Get DTS Test Variables")
def get_dts_test_variables(self) -> dict[PlatformName, PlatformVariables]:
"""Parses all files in 'platform-configs` except ones defined in
SKIP_PLATFORMS into dictionary with keys being a platform name and value
being dict of DTS_TEST_* variables.

Returns:
dict[PlatformName, PlatformVariables]: dict with DTS test variables: \
{<platform_name>: {<variable_name>: <variable_value>}}
"""
platforms = self._get_dts_test_platforms()
platform_variables: dict[PlatformName, PlatformVariables] = {}
for platform, namespace in platforms:
variables = self._get_variables_by_name(
namespace, lambda name: name.startswith("DTS_TEST")
)
platform_variables[platform] = variables
return platform_variables

@keyword("Get Platform Variables")
def get_platform_variables(self, platform: PlatformName) -> PlatformVariables:
"""Parse {platform}.robot in platform-configs and return dict containing
platform variables. Global variables are included but overwritten by
platform if it also defines them

Args:
platform (PlatformName): platform filename without extension

Returns:
PlatformVariables: dict with variables
"""
root = Path(BuiltIn().get_variable_value("${EXECDIR}"))
platform_path = root / f"platform-configs/{platform}.robot"
platform_namespace = self._get_resource(platform_path)
if not platform_namespace:
raise Error(f"Couldn't parse {platform_path}, it isn't a file")
self._update_namespace_with_global_vars(platform_namespace)
return self._get_variables_from_namespace(platform_namespace)

def _get_dts_test_platforms(self) -> list[tuple[PlatformName, Namespace]]:
"""Returns list of platforms that support DTS testing.
Checks all robot files in 'platform-configs' and parses each one into
Namespace containing all resources (e.g. variables)

Returns:
list[[tuple[PlatformName, Namespace]]: list of platforms that support
DTS testing. Each tuple contains robot filename without extension and
parsed resource returned as a Namespace
"""
root = Path(BuiltIn().get_variable_value("${EXECDIR}"))
platform_dir = root / "platform-configs"
dts_test_platforms = []
for platform in platform_dir.glob("*.robot"):
if platform.stem in self.SKIP_PLATFORMS:
continue
platform_namespace = self._get_resource(platform)
if not platform_namespace:
continue
self._update_namespace_with_global_vars(platform_namespace)
if is_truthy(self._get_variable_value(platform_namespace, "DTS_SUPPORT")):
dts_test_platforms.append((platform.stem, platform_namespace))
return dts_test_platforms

def _get_resource(self, platform: Path) -> Namespace | None:
"""Parse platform file into Namespace containing e.g. variables

Args:
platform (Path): Path to platform robot file to parse

Returns:
Namespace | None: Parsed namespace or None if path was not a file
"""
if not platform.is_file():
return None
resource: ResourceFile = IMPORTER.import_resource(str(platform))
platform_namespace = Namespace(
variables=Variables(),
suite=TestSuite(f"Temporary {platform.stem} namespace"),
resource=resource,
languages=None,
)
platform_namespace.variables.set_from_variable_section(resource.variables)
platform_namespace.handle_imports()
return platform_namespace

def _get_variables_from_namespace(self, namespace: Namespace) -> PlatformVariables:
"""Turn namespace into resolved variables that can be used in tests

Args:
namespace (Namespace): namespace to turn into PlatformVariables

Returns:
PlatformVariables: dict with variables defined in platform config
"""
namespace.variables.resolve_delayed()
return namespace.variables.as_dict()

def _get_variable_value(
self, namespace: Namespace, variable_name: str
) -> PlatformVariableValue | None:
"""Return resolved value of variable or None if no variable defined

Args:
namespace (Namespace): Namespace from which to take variable
variable_name (str): Variable name

Returns:
PlatformVariableValue | None: Variable value or None if variable
doesn't exist
"""
namespace_variables = namespace.variables
variables = namespace_variables.store.data
if variable_name in variables:
var = variables[variable_name]
if isinstance(var, Resolvable):
return var.resolve(namespace_variables)
else:
return var

return None

def _get_variables_by_name(
self, namespace: Namespace, predicate: Callable[[str], bool]
) -> PlatformVariables:
"""Return all variables whose keys fulfiill predicate

Args:
namespace (Namespace): namespace from which to get variables
predicate (Callable[[str], bool]): predicate used to filter variables

Returns:
PlatformVariables: variables that fulfill predicate
"""
filtered_variables: PlatformVariables = {}
for variable_name in namespace.variables.store:
if not predicate(variable_name):
continue
resolved_value = self._get_variable_value(namespace, variable_name)
if resolved_value is not None:
filtered_variables[variable_name] = resolved_value
return filtered_variables

def _update_namespace_with_global_vars(
self, namespace: Namespace, global_overwrite: bool = False
):
"""Used so variables like ${FALSE} can be resolved. Adds global
variables to namespace.

Args:
namespace (Namespace): Namespace to which add global vars
global_overwrite (bool, optional): whether global variables should
overwrite platform ones if both contain the same variable
"""
variable_store = namespace.variables.store
if global_overwrite:
variable_store.update(BuiltIn()._variables._global.store)
else:
global_variables = BuiltIn()._variables._global.copy()
global_variables.store.update(variable_store)
variable_store.update(global_variables.store)


def _print(string):
BuiltIn().log_to_console(string)
89 changes: 89 additions & 0 deletions dts/TemplateSplit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from robot.api.deco import library
from robot.api.interfaces import ListenerV3
from robot.libraries.BuiltIn import BuiltIn
from robot.model.keyword import Keyword
from robot.model.tags import Tags
from robot.model.testsuite import TestSuite
from robot.running.model import TestCase


@library(scope="SUITE", version="7.1", listener="SELF")
class TemplateSplit(ListenerV3):
"""Generates separate tests for templates"""

ROBOT_LISTENER_API_VERSION = 3

def __init__(
self,
*,
setup_template: bool = False,
copy_tags: bool = True,
add_count_prefix: bool = True,
custom_prefix: str = "",
):
"""Generates separate tests for templates

Args:
setup_template (bool, optional): Run Test Setup/Teardown keywords \
for template. When set to False only generated tests will run \
Test Setup and Test Teardown. Defaults to False.
copy_tags (bool, optional): Should test created from template have \
the same tags as template. Defaults to True.
add_count_prefix (bool, optional): Adds test number to generated test \
name. Count is only incremented when generating new templated test. \
Defaults to True.
custom_prefix (str, optional): Add this prefix before test number. \
Test name ends up as '<custom_prefix><count>: <template_keyword>. \
Used only if add_count_prefix is True. Defaults to empty string
"""
super().__init__()
self.setup_template = setup_template
self.copy_tags = copy_tags
self.add_prefix = add_count_prefix
self.custom_prefix = custom_prefix
self.suite: TestSuite | None = None
self.test: TestCase | None = None
self.setup: Keyword | None = None
self.teardown: Keyword | None = None
self.tags: Tags | None = None
self.count = 1

def start_test(self, data, result):
if data.template:
self.test = data
self.setup = self.test.setup
self.teardown = self.test.teardown
if not self.setup_template:
self.test.setup = None
self.test.teardown = None
if self.copy_tags:
self.tags = data.tags

def end_test(self, data, result):
self.test = None
self.setup = None
self.teardown = None
self.tags = None

def start_user_keyword(self, data, implementation, result):
if self.test and data != self.setup and result.status != "NOT RUN":
keyword_name = BuiltIn().replace_variables(data.name)
if self.add_prefix:
new_test_name = f"{self.custom_prefix}{self.count:03}: {keyword_name}"
else:
new_test_name = str(keyword_name)
new_test: TestCase = self.test.parent.tests.create(
new_test_name, tags=self.tags
)
new_test.body.create_keyword(keyword_name, args=data.args)
if self.setup:
new_test.setup.config(name=self.setup.name, args=self.setup.args)
if self.teardown:
new_test.teardown.config(
name=self.teardown.name, args=self.teardown.args
)
implementation.body.clear()
implementation.body.create_keyword(
"Log", [f"Created test: {new_test_name}"]
)
self.count += 1
Loading
Loading