Thank you for addressing this. Great work! Reviewed-by: Nicholas Pratte <npra...@iol.unh.edu>
On Fri, Jan 24, 2025 at 6:39 AM Luca Vizzarro <luca.vizza...@arm.com> wrote: > > The current handling of the configuration loading is inconsistent. After > the whole configuration is loaded, if there are any CLI or environment > overrides set, the code forcefully modifies the frozen configuration to > use them. > > This changes the handling by passing the environment/CLI settings as > part of the configuration context and handle the overrides directly at > the field level before these are validated. As a positive side effect, > the validator won't complain if a field is missing from the file but it > has been specified as an environment/CLI override. > > Bugzilla ID: 1360 > Bugzilla ID: 1598 > > Signed-off-by: Nicholas Pratte <npra...@iol.unh.edu> > Signed-off-by: Luca Vizzarro <luca.vizza...@arm.com> > Reviewed-by: Paul Szczepanek <paul.szczepa...@arm.com> > Reviewed-by: Dean Marx <dm...@iol.unh.edu> > --- > dts/framework/config/__init__.py | 65 ++++++++++++++++++++++++++++++-- > dts/framework/runner.py | 18 ++------- > 2 files changed, 64 insertions(+), 19 deletions(-) > > diff --git a/dts/framework/config/__init__.py > b/dts/framework/config/__init__.py > index 2496f48e20..6ae98d0387 100644 > --- a/dts/framework/config/__init__.py > +++ b/dts/framework/config/__init__.py > @@ -33,10 +33,11 @@ > """ > > import tarfile > +from collections.abc import Callable, MutableMapping > from enum import Enum, auto, unique > from functools import cached_property > from pathlib import Path, PurePath > -from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple > +from typing import TYPE_CHECKING, Annotated, Any, Literal, NamedTuple, > TypedDict, cast > > import yaml > from pydantic import ( > @@ -44,18 +45,60 @@ > ConfigDict, > Field, > ValidationError, > + ValidationInfo, > field_validator, > model_validator, > ) > from typing_extensions import Self > > from framework.exception import ConfigurationError > +from framework.settings import Settings > from framework.utils import REGEX_FOR_PCI_ADDRESS, StrEnum > > if TYPE_CHECKING: > from framework.test_suite import TestSuiteSpec > > > +class ValidationContext(TypedDict): > + """A context dictionary to use for validation.""" > + > + #: The command line settings. > + settings: Settings > + > + > +def load_fields_from_settings( > + *fields: str | tuple[str, str], > +) -> Callable[[Any, ValidationInfo], Any]: > + """Before model validator that injects values from > :attr:`ValidationContext.settings`. > + > + Args: > + *fields: The name of the fields to apply the argument value to. If > the settings field name > + is not the same as the configuration field, supply a tuple with > the respective names. > + > + Returns: > + Pydantic before model validator. > + """ > + > + def _loader(data: Any, info: ValidationInfo) -> Any: > + if not isinstance(data, MutableMapping): > + return data > + > + settings = cast(ValidationContext, info.context)["settings"] > + for field in fields: > + if isinstance(field, tuple): > + settings_field = field[0] > + config_field = field[1] > + else: > + settings_field = config_field = field > + > + if settings_data := getattr(settings, settings_field): > + data[config_field] = settings_data > + > + return data > + > + return _loader > + > + > class FrozenModel(BaseModel): > """A pre-configured :class:`~pydantic.BaseModel`.""" > > @@ -317,6 +360,10 @@ class BaseDPDKBuildConfiguration(FrozenModel): > #: The location of the DPDK tree. > dpdk_location: DPDKLocation > > + dpdk_location_from_settings = model_validator(mode="before")( > + load_fields_from_settings("dpdk_location") > + ) > + > > class DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): > """DPDK precompiled build configuration.""" > @@ -325,6 +372,10 @@ class > DPDKPrecompiledBuildConfiguration(BaseDPDKBuildConfiguration): > #: subdirectory of `~dpdk_location.dpdk_tree` or > `~dpdk_location.tarball` root directory. > precompiled_build_dir: str = Field(min_length=1) > > + build_dir_from_settings = model_validator(mode="before")( > + load_fields_from_settings("precompiled_build_dir") > + ) > + > > class DPDKBuildOptionsConfiguration(FrozenModel): > """DPDK build options configuration. > @@ -439,6 +490,10 @@ class TestRunConfiguration(FrozenModel): > #: The seed to use for pseudo-random generation. > random_seed: int | None = None > > + fields_from_settings = model_validator(mode="before")( > + load_fields_from_settings("test_suites", "random_seed") > + ) > + > > class TestRunWithNodesConfiguration(NamedTuple): > """Tuple containing the configuration of the test run and its associated > nodes.""" > @@ -541,7 +596,7 @@ def validate_test_runs_with_nodes(self) -> Self: > return self > > > -def load_config(config_file_path: Path) -> Configuration: > +def load_config(settings: Settings) -> Configuration: > """Load DTS test run configuration from a file. > > Load the YAML test run configuration file, validate it, and create a > test run configuration > @@ -552,6 +607,7 @@ def load_config(config_file_path: Path) -> Configuration: > > Args: > config_file_path: The path to the YAML test run configuration file. > + settings: The settings provided by the user on the command line. > > Returns: > The parsed test run configuration. > @@ -559,10 +615,11 @@ def load_config(config_file_path: Path) -> > Configuration: > Raises: > ConfigurationError: If the supplied configuration file is invalid. > """ > - with open(config_file_path, "r") as f: > + with open(settings.config_file_path, "r") as f: > config_data = yaml.safe_load(f) > > try: > - return Configuration.model_validate(config_data) > + context = ValidationContext(settings=settings) > + return Configuration.model_validate(config_data, context=context) > except ValidationError as e: > raise ConfigurationError("failed to load the supplied > configuration") from e > diff --git a/dts/framework/runner.py b/dts/framework/runner.py > index 0cdbb07e06..e46a8c1a4f 100644 > --- a/dts/framework/runner.py > +++ b/dts/framework/runner.py > @@ -31,7 +31,6 @@ > > from .config import ( > Configuration, > - DPDKPrecompiledBuildConfiguration, > SutNodeConfiguration, > TestRunConfiguration, > TestSuiteConfig, > @@ -82,7 +81,7 @@ class DTSRunner: > > def __init__(self): > """Initialize the instance with configuration, logger, result and > string constants.""" > - self._configuration = load_config(SETTINGS.config_file_path) > + self._configuration = load_config(SETTINGS) > self._logger = get_dts_logger() > if not os.path.exists(SETTINGS.output_dir): > os.makedirs(SETTINGS.output_dir) > @@ -142,9 +141,7 @@ def run(self) -> None: > self._init_random_seed(test_run_config) > test_run_result = self._result.add_test_run(test_run_config) > # we don't want to modify the original config, so create a > copy > - test_run_test_suites = list( > - SETTINGS.test_suites if SETTINGS.test_suites else > test_run_config.test_suites > - ) > + test_run_test_suites = test_run_config.test_suites > if not test_run_config.skip_smoke_tests: > test_run_test_suites[:0] = > [TestSuiteConfig(test_suite="smoke_tests")] > try: > @@ -320,15 +317,6 @@ def _run_test_run( > test_run_result.sut_info = sut_node.node_info > try: > dpdk_build_config = test_run_config.dpdk_config > - if new_location := SETTINGS.dpdk_location: > - dpdk_build_config = dpdk_build_config.model_copy( > - update={"dpdk_location": new_location} > - ) > - if dir := SETTINGS.precompiled_build_dir: > - dpdk_build_config = DPDKPrecompiledBuildConfiguration( > - dpdk_location=dpdk_build_config.dpdk_location, > - precompiled_build_dir=dir, > - ) > sut_node.set_up_test_run(test_run_config, dpdk_build_config) > test_run_result.dpdk_build_info = sut_node.get_dpdk_build_info() > tg_node.set_up_test_run(test_run_config, dpdk_build_config) > @@ -622,6 +610,6 @@ def _exit_dts(self) -> None: > > def _init_random_seed(self, conf: TestRunConfiguration) -> None: > """Initialize the random seed to use for the test run.""" > - seed = SETTINGS.random_seed or conf.random_seed or > random.randrange(0xFFFF_FFFF) > + seed = conf.random_seed or random.randrange(0xFFFF_FFFF) > self._logger.info(f"Initializing test run with random seed {seed}.") > random.seed(seed) > -- > 2.43.0 >