Files
open-source-firmware-valida…/dts/PlatformParser.py
Michał Iwanicki 07cf214e4b Add templated DTS E2E tests (#863)
* dts: Add TemplateSplit and PlatformParser libraries

* TemplateSplit - split each template keyword into separate tests
* PlatformParser - parse platform config and return variables defined
  for that platform in a dict

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* requirements.txt: update to robotframework 7.3

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts: replace tests with template generated ones

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-lib: remove unused keyword

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-lib: Add some improvements and new keywords

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* platform-configs: add config used with templated E2E tests

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts: dts-e2e: review fixes

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: pre-commit fixes

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: fix email in spdx and remove requirements-rf7.txt

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: change subscription to release

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: Set default version for every workflow

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: docs: add templated test documentation

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

* dts-e2e: novacustom-nuc_box doesn't support any DTS workflows yet

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>

---------

Signed-off-by: Michał Iwanicki <michal.iwanicki@3mdeb.com>
2025-08-01 12:08:55 +02:00

206 lines
8.3 KiB
Python

# SPDX-FileCopyrightText: 2025 Michał Iwanicki <iwanicki92@gmail.com>
#
# SPDX-License-Identifier: Apache-2.0
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)