From 02d91d3bcb729240b6abadc5f9452ac863b140a2 Mon Sep 17 00:00:00 2001 From: Sveinung Gundersen Date: Mon, 9 Dec 2024 21:23:15 +0100 Subject: [PATCH] Refactor test_runtime --- src/omnipy/api/protocols/private/engine.py | 8 + src/omnipy/api/protocols/public/hub.py | 42 +- src/omnipy/engine/base.py | 8 + src/omnipy/hub/log/root_log.py | 4 + src/omnipy/modules/prefect/engine/prefect.py | 4 +- tests/hub/runtime/helpers/mocks.py | 138 +----- tests/hub/runtime/test_runtime.py | 474 +++++++++---------- 7 files changed, 296 insertions(+), 382 deletions(-) diff --git a/src/omnipy/api/protocols/private/engine.py b/src/omnipy/api/protocols/private/engine.py index cfefa133..b692ae6c 100644 --- a/src/omnipy/api/protocols/private/engine.py +++ b/src/omnipy/api/protocols/private/engine.py @@ -19,3 +19,11 @@ def set_config(self, config: IsEngineConfig) -> None: def set_registry(self, registry: IsRunStateRegistry | None) -> None: ... + + @property + def config(self) -> IsEngineConfig: + ... + + @property + def registry(self) -> IsRunStateRegistry | None: + ... diff --git a/src/omnipy/api/protocols/public/hub.py b/src/omnipy/api/protocols/public/hub.py index c65425a1..182056ef 100644 --- a/src/omnipy/api/protocols/public/hub.py +++ b/src/omnipy/api/protocols/public/hub.py @@ -25,6 +25,10 @@ class IsRootLogObjects(Protocol): def set_config(self, config: IsRootLogConfig) -> None: ... + @property + def config(self) -> IsRootLogConfig: + ... + class IsRuntimeConfig(Protocol): """""" @@ -35,19 +39,16 @@ class IsRuntimeConfig(Protocol): prefect: IsPrefectEngineConfig root_log: IsRootLogConfig - def __init__( - self, - job: IsJobConfig | None = None, # noqa - data: IsDataConfig | None = None, # noqa - engine: EngineChoice = EngineChoice.LOCAL, # noqa - local: IsLocalRunnerConfig | None = None, # noqa - prefect: IsPrefectEngineConfig | None = None, # noqa - root_log: IsRootLogConfig | None = None, # noqa - *args: object, - **kwargs: object) -> None: + def __init__(self, + job: IsJobConfig, + data: IsDataConfig, + engine: EngineChoice, + local: IsLocalRunnerConfig, + prefect: IsPrefectEngineConfig, + root_log: IsRootLogConfig) -> None: ... - def reset_to_defaults(self): + def reset_to_defaults(self) -> None: ... @@ -62,17 +63,14 @@ class IsRuntimeObjects(Protocol): serializers: IsSerializerRegistry root_log: IsRootLogObjects - def __init__( - self, - job_creator: IsJobConfigHolder | None = None, # noqa - data_class_creator: IsDataClassCreator | None = None, # noqa - local: IsEngine | None = None, # noqa - prefect: IsEngine | None = None, # noqa - registry: IsRunStateRegistry | None = None, # noqa - serializers: IsSerializerRegistry | None = None, # noqa - root_log: IsRootLogObjects | None = None, # noqa - *args: object, - **kwargs: object) -> None: + def __init__(self, + job_creator: IsJobConfigHolder, + data_class_creator: IsDataClassCreator, + local: IsEngine, + prefect: IsEngine, + registry: IsRunStateRegistry, + serializers: IsSerializerRegistry, + root_log: IsRootLogObjects) -> None: ... diff --git a/src/omnipy/engine/base.py b/src/omnipy/engine/base.py index ec43de88..22690c63 100644 --- a/src/omnipy/engine/base.py +++ b/src/omnipy/engine/base.py @@ -46,3 +46,11 @@ def set_config(self, config: IsEngineConfig) -> None: def set_registry(self, registry: IsRunStateRegistry | None) -> None: self._registry = registry + + @property + def config(self) -> IsEngineConfig: + return self._config + + @property + def registry(self) -> IsRunStateRegistry | None: + return self._registry diff --git a/src/omnipy/hub/log/root_log.py b/src/omnipy/hub/log/root_log.py index 0d10148f..cb1b2fd0 100644 --- a/src/omnipy/hub/log/root_log.py +++ b/src/omnipy/hub/log/root_log.py @@ -33,6 +33,10 @@ def set_config(self, config: IsRootLogConfig): self._config = config self._configure_all_objects() + @property + def config(self) -> IsRootLogConfig: + return self._config + def _configure_all_objects(self): self._remove_all_handlers_from_root_logger() diff --git a/src/omnipy/modules/prefect/engine/prefect.py b/src/omnipy/modules/prefect/engine/prefect.py index f5a5d6be..4dbc716b 100644 --- a/src/omnipy/modules/prefect/engine/prefect.py +++ b/src/omnipy/modules/prefect/engine/prefect.py @@ -33,12 +33,12 @@ def _update_from_config(self) -> None: @classmethod def get_config_cls(cls) -> Type[IsPrefectEngineConfig]: - return PrefectEngineConfig + return PrefectEngineConfigEntryPublisher # TaskRunnerEngine def _init_task(self, task: IsTask, call_func: Callable) -> PrefectTask: - assert isinstance(self._config, PrefectEngineConfig) + assert isinstance(self._config, PrefectEngineConfigEntryPublisher) task_kwargs = dict( name=task.name, cache_key_fn=task_input_hash if self._config.use_cached_results else None, diff --git a/tests/hub/runtime/helpers/mocks.py b/tests/hub/runtime/helpers/mocks.py index 40f2c771..7976d566 100644 --- a/tests/hub/runtime/helpers/mocks.py +++ b/tests/hub/runtime/helpers/mocks.py @@ -1,67 +1,16 @@ from abc import ABC, abstractmethod from dataclasses import dataclass -from datetime import datetime -from typing import Protocol, Type +from typing import cast, Generic, Protocol -from omnipy.api.enums import RunState -from omnipy.api.protocols.private.log import IsRunStateRegistry -from omnipy.api.protocols.public.compute import IsTask -from omnipy.api.protocols.public.config import (IsEngineConfig, - IsLocalRunnerConfig, - IsPrefectEngineConfig, - IsRootLogConfig) -from omnipy.api.protocols.public.engine import IsTaskRunnerEngine - - -class IsMockJobConfig(Protocol): - persist_outputs: bool = True - restore_outputs: bool = False - - -@dataclass -class MockJobConfig: - persist_outputs: bool = True - restore_outputs: bool = False - - -class MockJobCreator: - def __init__(self) -> None: - self.engine: IsTaskRunnerEngine | None = None - self.config: IsMockJobConfig = MockJobConfig() - - def set_engine(self, engine: IsTaskRunnerEngine) -> None: - self.engine = engine - - def set_config(self, config: IsMockJobConfig) -> None: - self.config = config - - -class MockJobCreator2(MockJobCreator): - ... - - -class IsMockDataConfig(Protocol): - interactive_mode: bool = True - - -@dataclass -class MockDataConfig: - interactive_mode: bool = True - - -class MockDataClassCreator: - def __init__(self) -> None: - self.config: IsMockDataConfig = MockDataConfig() - - def set_config(self, config: IsMockDataConfig) -> None: - self.config = config +from typing_extensions import TypeVar +from omnipy.api.protocols.private.log import IsRunStateRegistry +from omnipy.api.protocols.public.config import IsEngineConfig -class MockDataClassCreator2(MockDataClassCreator): - ... +EngineConfigT = TypeVar('EngineConfigT', bound=IsEngineConfig) -class MockEngine(ABC): +class MockEngine(ABC, Generic[EngineConfigT]): def __init__(self) -> None: config_cls = self.get_config_cls() self._config: IsEngineConfig = config_cls() @@ -69,7 +18,7 @@ def __init__(self) -> None: @classmethod @abstractmethod - def get_config_cls(cls) -> Type[IsEngineConfig]: + def get_config_cls(cls) -> type[EngineConfigT]: ... def set_config(self, config: IsEngineConfig) -> None: @@ -84,81 +33,36 @@ class MockLocalRunnerConfig: backend_verbose: bool = True -class MockLocalRunnerConfig2(MockLocalRunnerConfig): - ... +class IsMockLocalRunnerConfig(IsEngineConfig, Protocol): + backend_verbose: bool -class MockLocalRunner(MockEngine): +class MockLocalRunner(MockEngine[IsMockLocalRunnerConfig]): @classmethod - def get_config_cls(cls) -> Type[IsEngineConfig]: + def get_config_cls(cls) -> type[IsMockLocalRunnerConfig]: return MockLocalRunnerConfig @property - def config(self) -> IsLocalRunnerConfig: - return self._config - - -class MockLocalRunner2(MockLocalRunner): - @classmethod - def get_config_cls(cls) -> Type[IsEngineConfig]: - return MockLocalRunnerConfig2 + def config(self) -> IsMockLocalRunnerConfig: + return cast(IsMockLocalRunnerConfig, self._config) @dataclass class MockPrefectEngineConfig: server_url: str = '' + use_cached_results: bool = False -class MockPrefectEngineConfig2(MockPrefectEngineConfig): - ... +class IsMockPrefectEngineConfig(IsEngineConfig, Protocol): + server_url: str = '' + use_cached_results: bool -class MockPrefectEngine(MockEngine): +class MockPrefectEngine(MockEngine[IsMockPrefectEngineConfig]): @classmethod - def get_config_cls(cls) -> Type[IsEngineConfig]: + def get_config_cls(cls) -> type[IsMockPrefectEngineConfig]: return MockPrefectEngineConfig @property - def config(self) -> IsPrefectEngineConfig: - return self._config - - -class MockPrefectEngine2(MockPrefectEngine): - @classmethod - def get_config_cls(cls) -> Type[IsEngineConfig]: - return MockPrefectEngineConfig2 - - -class MockRootLogObjects: - def __init__(self): - self.config: IsRootLogConfig | None = None - - def set_config(self, config: IsRootLogConfig): - self.config = config - - -class MockRootLogObjects2(MockRootLogObjects): - ... - - -@dataclass -class MockRootLogConfig: - log_to_stderr: bool = True - - -class MockRunStateRegistry: - def get_task_state(self, task: IsTask) -> RunState: - ... - - def get_task_state_datetime(self, task: IsTask, state: RunState) -> datetime: - ... - - def all_tasks(self, state: RunState | None = None) -> tuple[IsTask, ...]: # noqa - ... - - def set_task_state(self, task: IsTask, state: RunState) -> None: - ... - - -class MockRunStateRegistry2(MockRunStateRegistry): - ... + def config(self) -> IsMockPrefectEngineConfig: + return cast(IsMockPrefectEngineConfig, self._config) diff --git a/tests/hub/runtime/test_runtime.py b/tests/hub/runtime/test_runtime.py index a4b01db4..6032fef4 100644 --- a/tests/hub/runtime/test_runtime.py +++ b/tests/hub/runtime/test_runtime.py @@ -4,47 +4,36 @@ from typing import Annotated, Type import pytest +import pytest_cases as pc from omnipy.api.enums import (BackoffStrategy, ConfigOutputStorageProtocolOptions, ConfigPersistOutputsOptions, ConfigRestoreOutputsOptions, EngineChoice) +from omnipy.api.protocols.public.config import IsEngineConfig from omnipy.api.protocols.public.hub import IsRuntime, IsRuntimeConfig +from omnipy.compute.job import JobBase +from omnipy.compute.job_creator import JobCreator from omnipy.config.data import DataConfig -from omnipy.data.data_class_creator import DataClassBase +from omnipy.config.engine import LocalRunnerConfig, PrefectEngineConfig +from omnipy.config.job import JobConfig +from omnipy.data.data_class_creator import DataClassBase, DataClassCreator +from omnipy.data.serializer import SerializerRegistry +from omnipy.engine.local import LocalRunner, LocalRunnerConfigEntryPublisher +from omnipy.hub.log.root_log import RootLogConfigEntryPublisher, RootLogObjects +from omnipy.hub.registry import RunStateRegistry from omnipy.hub.runtime import RuntimeConfig, RuntimeObjects +from omnipy.modules.prefect.engine.prefect import PrefectEngine, PrefectEngineConfigEntryPublisher -from .helpers.mocks import (MockDataClassCreator, - MockDataClassCreator2, - MockDataConfig, - MockJobConfig, - MockJobCreator, - MockJobCreator2, - MockLocalRunner, - MockLocalRunner2, +from .helpers.mocks import (MockLocalRunner, MockLocalRunnerConfig, - MockLocalRunnerConfig2, MockPrefectEngine, - MockPrefectEngine2, - MockPrefectEngineConfig, - MockPrefectEngineConfig2, - MockRootLogConfig, - MockRootLogObjects, - MockRootLogObjects2, - MockRunStateRegistry, - MockRunStateRegistry2) + MockPrefectEngineConfig) def _assert_runtime_config_default(config: IsRuntimeConfig, dir_path: Path): - from omnipy.config.engine import LocalRunnerConfig, PrefectEngineConfig - from omnipy.config.job import JobConfig - assert isinstance(config.job, JobConfig) - assert isinstance(config.data, DataConfig) - assert isinstance(config.local, LocalRunnerConfig) - assert isinstance(config.prefect, PrefectEngineConfig) - assert config.job.output_storage.persist_outputs == \ ConfigPersistOutputsOptions.ENABLE_FLOW_AND_TASK_OUTPUTS assert config.job.output_storage.restore_outputs == \ @@ -57,6 +46,8 @@ def _assert_runtime_config_default(config: IsRuntimeConfig, dir_path: Path): assert config.job.output_storage.s3.bucket_name == '' assert config.job.output_storage.s3.access_key == '' assert config.job.output_storage.s3.secret_key == '' + + assert isinstance(config.data, DataConfig) assert config.data.interactive_mode is True assert config.data.dynamically_convert_elements_to_models is False assert config.data.terminal_size_columns == 80 @@ -67,20 +58,14 @@ def _assert_runtime_config_default(config: IsRuntimeConfig, dir_path: Path): assert config.data.http_defaults.retry_attempts == 5 assert config.data.http_defaults.retry_backoff_strategy == BackoffStrategy.EXPONENTIAL assert isinstance(config.data.http_config_for_host, defaultdict) + assert config.engine == EngineChoice.LOCAL + assert isinstance(config.local, LocalRunnerConfig) + assert isinstance(config.prefect, PrefectEngineConfig) assert config.prefect.use_cached_results is False def _assert_runtime_objects_default(objects: RuntimeObjects): - from omnipy.compute.job import JobBase - from omnipy.compute.job_creator import JobCreator - from omnipy.data.data_class_creator import DataClassBase, DataClassCreator - from omnipy.data.serializer import SerializerRegistry - from omnipy.engine.local import LocalRunner - from omnipy.hub.log.root_log import RootLogObjects - from omnipy.hub.registry import RunStateRegistry - from omnipy.modules.prefect.engine.prefect import PrefectEngine - assert isinstance(objects.job_creator, JobCreator) assert objects.job_creator is JobBase.job_creator @@ -123,240 +108,247 @@ def test_data_config_http_config_for_host_default( .requests_per_time_period == 60 -def test_runtime_config_after_data_class_creator( +def test_init_runtime_config_after_data_class_creator( runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - DataClassBase.data_class_creator.config.dynamically_convert_elements_to_models = True - DataClassBase.data_class_creator.config.terminal_size_columns = 100 + DataClassBase.data_class_creator.config.dynamically_convert_elements_to_models = True runtime = runtime_cls() assert runtime.config.data.dynamically_convert_elements_to_models is True - assert runtime.config.data.terminal_size_columns == 100 runtime.config.reset_to_defaults() _assert_runtime_config_default(runtime.config, Path.cwd()) - assert DataClassBase.data_class_creator.config.dynamically_convert_elements_to_models is False - assert DataClassBase.data_class_creator.config.terminal_size_columns == 80 -def test_engines_subscribe_to_registry( +@pytest.mark.skipif(os.getenv('OMNIPY_FORCE_SKIPPED_TEST') != '1', reason="""Postponed for now""") +def test_init_runtime_config_after_job_creator( runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_local_runner = MockLocalRunner() - mock_prefect_engine = MockPrefectEngine() - mock_registry = MockRunStateRegistry() - - runtime = runtime_cls( - objects=RuntimeObjects( - local=mock_local_runner, - prefect=mock_prefect_engine, - registry=mock_registry, - )) - - assert runtime.objects.local is mock_local_runner - assert runtime.objects.prefect is mock_prefect_engine - - assert runtime.objects.local.registry is runtime.objects.registry - assert runtime.objects.prefect.registry is runtime.objects.registry - - mock_local_runner_2 = MockLocalRunner() - mock_prefect_engine_2 = MockPrefectEngine() - runtime.objects.local = mock_local_runner_2 - runtime.objects.prefect = mock_prefect_engine_2 - assert runtime.objects.local is mock_local_runner_2 is not mock_local_runner - assert runtime.objects.prefect is mock_prefect_engine_2 is not mock_prefect_engine - assert runtime.objects.local.registry is runtime.objects.registry - assert runtime.objects.prefect.registry is runtime.objects.registry - - mock_run_state_registry_2 = MockRunStateRegistry2() - assert mock_run_state_registry_2 is not runtime.objects.registry - runtime.objects.registry = mock_run_state_registry_2 - - assert runtime.objects.local.registry is runtime.objects.registry - assert runtime.objects.prefect.registry is runtime.objects.registry - - -def test_engines_subscribe_to_config( - runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_local_runner = MockLocalRunner() - mock_prefect_engine = MockPrefectEngine() - runtime = runtime_cls( - objects=RuntimeObjects( - local=mock_local_runner, - prefect=mock_prefect_engine, - )) - - assert isinstance(runtime.config.local, MockLocalRunnerConfig) - assert isinstance(runtime.config.prefect, MockPrefectEngineConfig) - assert runtime.objects.local.config is runtime.config.local - assert runtime.objects.prefect.config is runtime.config.prefect - assert runtime.objects.local.config.backend_verbose is True - assert runtime.objects.prefect.config.server_url == '' - - runtime.config.local.backend_verbose = False - runtime.config.prefect.server_url = 'https://my.prefectserver.nowhere' - assert runtime.objects.local.config.backend_verbose is False - assert runtime.objects.prefect.config.server_url == 'https://my.prefectserver.nowhere' - - mock_local_runner_config = MockLocalRunnerConfig(backend_verbose=True) - mock_prefect_engine_config = MockPrefectEngineConfig( - server_url='https://another.prefectserver.nowhere') - runtime.config.local = mock_local_runner_config - runtime.config.prefect = mock_prefect_engine_config - - assert runtime.objects.local.config is runtime.config.local is mock_local_runner_config - assert runtime.objects.prefect.config is runtime.config.prefect is mock_prefect_engine_config - assert runtime.objects.local.config.backend_verbose is True - assert runtime.objects.prefect.config.server_url == 'https://another.prefectserver.nowhere' - - runtime.objects.local = MockLocalRunner() - runtime.objects.prefect = MockPrefectEngine() - assert runtime.objects.local.config is runtime.config.local is mock_local_runner_config - assert runtime.objects.prefect.config is runtime.config.prefect is mock_prefect_engine_config - assert runtime.objects.local.config.backend_verbose is True - assert runtime.objects.prefect.config.server_url == 'https://another.prefectserver.nowhere' - - runtime.objects.local = MockLocalRunner2() - runtime.objects.prefect = MockPrefectEngine2() - assert isinstance(runtime.config.local, MockLocalRunnerConfig2) - assert isinstance(runtime.config.prefect, MockPrefectEngineConfig2) - assert runtime.config.local is not mock_local_runner_config - assert runtime.config.prefect is not mock_prefect_engine_config - - -def test_root_log_object_subscribe_to_config( - runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_root_log_objects = MockRootLogObjects() - mock_root_log_config = MockRootLogConfig(log_to_stderr=False) - runtime = runtime_cls( - objects=RuntimeObjects(root_log=mock_root_log_objects), - config=RuntimeConfig(root_log=mock_root_log_config), - ) - assert runtime.objects.root_log.config is runtime.config.root_log is mock_root_log_config - assert runtime.objects.root_log.config.log_to_stderr is False - - runtime.config.root_log.log_to_stderr = True - assert runtime.objects.root_log.config.log_to_stderr is True - - mock_root_log_config_2 = MockRootLogConfig(log_to_stderr=False) - assert mock_root_log_config_2 is not mock_root_log_config - runtime.config.root_log = mock_root_log_config_2 - assert runtime.objects.root_log.config is runtime.config.root_log is mock_root_log_config_2 - assert runtime.objects.root_log.config.log_to_stderr is False - - runtime.objects.root_log = MockRootLogObjects() - assert runtime.objects.root_log.config is runtime.config.root_log is mock_root_log_config_2 - assert runtime.objects.root_log.config.log_to_stderr is False + JobBase.job_creator.config.output_storage.persist_outputs = 'disabled' + runtime = runtime_cls() - runtime.objects.root_log = MockRootLogObjects2() - assert runtime.config.root_log is mock_root_log_config_2 + assert runtime.config.job.output_storage.persist_outputs == 'disabled' + runtime.config.reset_to_defaults() -def test_job_creator_subscribe_to_engine( - runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_job_creator = MockJobCreator() - mock_local_runner = MockLocalRunner() - mock_prefect_engine = MockPrefectEngine() - runtime = runtime_cls( - objects=RuntimeObjects( - job_creator=mock_job_creator, - local=mock_local_runner, - prefect=mock_prefect_engine, + _assert_runtime_config_default(runtime.config, Path.cwd()) + assert runtime.config.job.output_storage.persist_outputs == 'all' + + +@pc.parametrize( + argnames=[ + 'publisher_runtime_attr_names', + 'publisher_cls', + 'subscriber_runtime_attr_names', + 'subscriber_cls', + 'subscriber_attr_name', + ], + argvalues=[ + ( + ('config', 'job'), + JobConfig, + ('objects', 'job_creator'), + JobCreator, + 'config', + ), + ( + ('config', 'data'), + DataConfig, + ('objects', 'data_class_creator'), + DataClassCreator, + 'config', + ), + ( + ('config', 'local'), + LocalRunnerConfigEntryPublisher, + ('objects', 'local'), + LocalRunner, + 'config', + ), + ( + ('config', 'prefect'), + PrefectEngineConfigEntryPublisher, + ('objects', 'prefect'), + PrefectEngine, + 'config', + ), + ( + ('config', 'root_log'), + RootLogConfigEntryPublisher, + ('objects', 'root_log'), + RootLogObjects, + 'config', + ), + ( + ('objects', 'registry'), + RunStateRegistry, + ('objects', 'local'), + LocalRunner, + 'registry', ), - config=RuntimeConfig(engine=EngineChoice.PREFECT), - ) + ( + ('objects', 'registry'), + RunStateRegistry, + ('objects', 'prefect'), + PrefectEngine, + 'registry', + ), + ], + ids=[ + 'config->job => objects->job_creator', + 'config->data => objects->data_class_creator', + 'config->local => objects->local', + 'config->prefect => objects->prefect', + 'config->root_log => objects->root_log', + 'objects->registry => objects->local', + 'objects->registry => objects->prefect', + ]) +def test_basic_runtime_subscriptions( + runtime: Annotated[IsRuntime, pytest.fixture], + publisher_runtime_attr_names: tuple[str, ...], + publisher_cls: type, + subscriber_runtime_attr_names: tuple[str, ...], + subscriber_cls: type, + subscriber_attr_name: str, +) -> None: + def _get_runtime_nested_attr(attr_names: tuple[str, ...]) -> object: + obj = runtime + for attr_name in attr_names: + obj = getattr(obj, attr_name) + return obj + + def _publisher() -> object: + return _get_runtime_nested_attr(publisher_runtime_attr_names) + + def _subscriber() -> object: + return _get_runtime_nested_attr(subscriber_runtime_attr_names) + + def _subscriber_attr(subscriber: object) -> object: + return getattr(subscriber, subscriber_attr_name) + + publisher = _publisher() + publisher_parent = _get_runtime_nested_attr(publisher_runtime_attr_names[:-1]) + publisher_parent_attr_name = publisher_runtime_attr_names[-1] + + subscriber = _subscriber() + subscriber_parent = _get_runtime_nested_attr(subscriber_runtime_attr_names[:-1]) + subscriber_parent_attr_name = subscriber_runtime_attr_names[-1] + + assert isinstance(publisher, publisher_cls) + assert isinstance(subscriber, subscriber_cls) + assert _subscriber_attr(subscriber) is publisher + + subscriber_2 = subscriber_cls() + setattr(subscriber_parent, subscriber_parent_attr_name, subscriber_2) + assert _subscriber() is subscriber_2 is not subscriber + assert _subscriber_attr(subscriber_2) is publisher + + publisher_2 = publisher_cls() + setattr(publisher_parent, publisher_parent_attr_name, publisher_2) + assert _publisher() is publisher_2 is not publisher + assert _subscriber() is subscriber_2 is not subscriber + assert _subscriber_attr(subscriber_2) is publisher_2 is not publisher + + setattr(subscriber_parent, subscriber_parent_attr_name, subscriber) + assert _subscriber() is subscriber is not subscriber_2 + assert _subscriber_attr(subscriber) is publisher_2 + + +def test_job_creator_subscribes_to_selected_engine( + runtime: Annotated[IsRuntime, pytest.fixture]) -> None: - assert isinstance(runtime.objects.job_creator, MockJobCreator) - assert isinstance(runtime.objects.local, MockLocalRunner) - assert isinstance(runtime.objects.prefect, MockPrefectEngine) - assert isinstance(runtime.config.engine, str) + local_runner = runtime.objects.local + prefect_engine = runtime.objects.prefect - assert runtime.config.engine == EngineChoice.PREFECT - assert runtime.objects.job_creator.engine is runtime.objects.prefect + assert isinstance(runtime.objects.job_creator, JobCreator) + assert isinstance(local_runner, LocalRunner) + assert isinstance(prefect_engine, PrefectEngine) + assert isinstance(runtime.config.engine, str) - runtime.config.engine = EngineChoice.LOCAL - assert runtime.objects.job_creator.engine is runtime.objects.local + assert runtime.config.engine == EngineChoice.LOCAL + assert runtime.objects.job_creator.engine is local_runner runtime.config.engine = EngineChoice.PREFECT - assert runtime.objects.job_creator.engine is runtime.objects.prefect + assert runtime.objects.job_creator.engine is prefect_engine - mock_local_runner_2 = MockLocalRunner() - mock_prefect_engine_2 = MockPrefectEngine() - assert mock_local_runner_2 is not mock_local_runner - assert mock_prefect_engine_2 is not mock_prefect_engine + runtime.config.engine = EngineChoice.LOCAL + assert runtime.objects.job_creator.engine is local_runner - runtime.objects.local = mock_local_runner_2 - runtime.objects.prefect = mock_prefect_engine_2 + local_runner_2 = LocalRunner() + prefect_engine_2 = PrefectEngine() + assert local_runner_2 is not local_runner + assert prefect_engine_2 is not prefect_engine - assert runtime.config.engine == EngineChoice.PREFECT - assert runtime.objects.job_creator.engine is runtime.objects.prefect is mock_prefect_engine_2 + runtime.objects.local = local_runner_2 + runtime.objects.prefect = prefect_engine_2 - runtime.config.engine = EngineChoice.LOCAL - assert runtime.objects.job_creator.engine is runtime.objects.local is mock_local_runner_2 + assert runtime.config.engine == EngineChoice.LOCAL + assert runtime.objects.job_creator.engine is runtime.objects.local is local_runner_2 runtime.config.engine = EngineChoice.PREFECT - runtime.objects.job_creator = MockJobCreator2() - assert runtime.objects.job_creator.engine is runtime.objects.prefect is mock_prefect_engine_2 - + assert runtime.objects.job_creator.engine is runtime.objects.prefect is prefect_engine_2 -def test_job_creator_subscribe_to_job_config( - runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_job_creator = MockJobCreator() - mock_job_config = MockJobConfig(persist_outputs=False, restore_outputs=True) - runtime = runtime_cls( - objects=RuntimeObjects(job_creator=mock_job_creator), - config=RuntimeConfig(job=mock_job_config), - ) - assert runtime.objects.job_creator.config is runtime.config.job is mock_job_config - assert runtime.objects.job_creator.config.persist_outputs is False - assert runtime.objects.job_creator.config.restore_outputs is True - - runtime.config.job.persist_outputs = True - assert runtime.objects.job_creator.config.persist_outputs is True - runtime.config.job.restore_outputs = False - assert runtime.objects.job_creator.config.restore_outputs is False - - mock_job_config_2 = MockJobConfig(persist_outputs=False, restore_outputs=True) - assert mock_job_config_2 is not mock_job_config - runtime.config.job = mock_job_config_2 - assert runtime.objects.job_creator.config is runtime.config.job is mock_job_config_2 - assert runtime.objects.job_creator.config.persist_outputs is False - assert runtime.objects.job_creator.config.restore_outputs is True - - runtime.objects.job_creator = MockJobCreator() - assert runtime.objects.job_creator.config is runtime.config.job is mock_job_config_2 - assert runtime.objects.job_creator.config.persist_outputs is False - assert runtime.objects.job_creator.config.restore_outputs is True - - runtime.objects.job_creator = MockJobCreator2() - assert runtime.config.job is mock_job_config_2 - - -def test_data_class_creator_subscribe_to_data_config( - runtime_cls: Annotated[Type[IsRuntime], pytest.fixture]) -> None: - mock_data_class_creator = MockDataClassCreator() - mock_data_config = MockDataConfig(interactive_mode=False) - runtime = runtime_cls( - objects=RuntimeObjects(data_class_creator=mock_data_class_creator), - config=RuntimeConfig(data=mock_data_config), - ) - assert runtime.objects.data_class_creator.config is runtime.config.data is mock_data_config - assert runtime.objects.data_class_creator.config.interactive_mode is False - - runtime.config.data.interactive_mode = True - assert runtime.objects.data_class_creator.config.interactive_mode is True - - mock_data_config_2 = MockDataConfig(interactive_mode=False) - assert mock_data_config_2 is not mock_data_config - runtime.config.data = mock_data_config_2 - assert runtime.objects.data_class_creator.config is runtime.config.data is mock_data_config_2 - assert runtime.objects.data_class_creator.config.interactive_mode is False - - runtime.objects.data_class_creator = MockDataClassCreator() - assert runtime.objects.data_class_creator.config is runtime.config.data is mock_data_config_2 - assert runtime.objects.data_class_creator.config.interactive_mode is False - - runtime.objects.data_class_creator = MockDataClassCreator2() - assert runtime.config.data is mock_data_config_2 + runtime.config.engine = EngineChoice.LOCAL + runtime.objects.job_creator = JobCreator() + assert runtime.objects.job_creator.engine is runtime.objects.local is local_runner_2 + + +@pc.parametrize( + 'engine_name, engine_cls, config_cls, mock_engine_cls, mock_config_class', + [ + ('local', + LocalRunner, + LocalRunnerConfigEntryPublisher, + MockLocalRunner, + MockLocalRunnerConfig), + ('prefect', + PrefectEngine, + PrefectEngineConfigEntryPublisher, + MockPrefectEngine, + MockPrefectEngineConfig), + ], + ids=['local', 'prefect']) +def test_new_engine_object_updates_engine_config_if_needed( + runtime: Annotated[IsRuntime, pytest.fixture], + engine_name: str, + engine_cls: type, + config_cls: type, + mock_engine_cls: type, + mock_config_class: type, +) -> None: + def _get_engine_config() -> IsEngineConfig: + return getattr(runtime.config, engine_name) + + def _get_engine_object() -> IsEngineConfig: + return getattr(runtime.objects, engine_name) + + def _set_engine_object(value) -> IsEngineConfig: + return setattr(runtime.objects, engine_name, value) + + assert isinstance(_get_engine_object(), engine_cls) + assert isinstance(_get_engine_config(), config_cls) + + config = _get_engine_config() + _set_engine_object(engine_cls()) + + assert isinstance(_get_engine_config(), config_cls) + assert _get_engine_config() is config + + _set_engine_object(mock_engine_cls()) + + assert isinstance(_get_engine_config(), mock_config_class) + assert _get_engine_config() is not config + + mock_config = _get_engine_config() + _set_engine_object(mock_engine_cls()) + + assert isinstance(_get_engine_config(), mock_config_class) + assert _get_engine_config() is mock_config + + _set_engine_object(engine_cls()) + + assert isinstance(_get_engine_config(), config_cls) + assert _get_engine_config() is not config + assert _get_engine_config() is not mock_config