"""yuxi.channels.infrastructure.host_bootstrap 单元测试。 覆盖 ``HostBootstrap`` 启动编排行为: - ``bootstrap`` 入口守卫:重复调用 / 已 ready 时抛 ``InternalError`` - ``_loadConfig``:空 schema 抛 ``ConfigValidationError``、``DependencyError`` 透传、 超时抛 ``OperationTimeoutError`` - ``_initSchema``:失败透传、超时抛 ``OperationTimeoutError`` - ``_initDrivenAdapters``:``ping()`` 失败抛 ``DependencyError``、 非 ``PingablePort`` 适配器跳过、空列表返回 - ``_loadPlugins``:discover 失败告警并继续、空 manifest 提前返回 - ``_loadSinglePlugin``:加载失败告警并继续(不阻塞宿主启动) - ``_rollback``:按 §15.2 逆序回滚已启动组件 - ``bootstrap`` 端到端:成功路径与失败回滚路径 不依赖运行中的 Docker 服务,纯单元测试。 """ from __future__ import annotations import asyncio from unittest.mock import AsyncMock, MagicMock import pytest from yuxi.channels.application.extension.config_source_registry import ( ConfigSourceRegistry, ) from yuxi.channels.application.extension.event_bus import EventBus from yuxi.channels.application.extension.event_subscription_registry import ( EventSubscriptionRegistry, ) from yuxi.channels.application.extension.stage_slot_registry import StageSlotRegistry from yuxi.channels.application.health.health_aggregator import HealthAggregator from yuxi.channels.application.lifecycle.plugin_dependency_resolver import ( PluginDependencyResolver, ) from yuxi.channels.application.lifecycle.plugin_lifecycle_manager import ( PluginLifecycleManager, ) from yuxi.channels.application.pipeline.stage_slot_injector import StageSlotInjector from yuxi.channels.application.transport import TransportManager from yuxi.channels.contract.dtos.lifecycle import LifecycleResult from yuxi.channels.contract.errors import ( ConfigValidationError, DependencyError, InternalError, OperationTimeoutError, ) from yuxi.channels.contract.ports.driven.config_port import ConfigPort from yuxi.channels.contract.ports.driven.logger_port import LoggerPort from yuxi.channels.contract.ports.driven.persistence_port import PersistencePort from yuxi.channels.contract.ports.driven.pingable_port import PingablePort from yuxi.channels.core.registry.plugin_registry import PluginRegistry from yuxi.channels.core.registry.route_match_registry import RouteMatchRegistry from yuxi.channels.core.service.degradation_manager import DegradationManager from yuxi.channels.infrastructure.dependency_injection import ( DependencyInjectionContainer, ) from yuxi.channels.infrastructure.host_bootstrap import HostBootstrap pytestmark = pytest.mark.unit # --------------------------------------------------------------------------- # 测试辅助:构造 HostBootstrap 与桩依赖 # --------------------------------------------------------------------------- def _make_manifest(plugin_id: str = "test-plugin"): """构造测试用 manifest 桩(仅需 ``id`` 属性)。 使用 ``MagicMock`` 替代真实 ``ChannelManifest``,避免构造 ``provides`` / ``capabilities`` / ``config_schema`` 等大量必填字段。测试仅需访问 ``manifest.id`` 属性。 """ manifest = MagicMock() manifest.id = plugin_id return manifest def _make_persistence_port_stub() -> MagicMock: """构造 ``PersistencePort`` 桩,``listRouteBindings`` 返回空列表。 ``_loadRouteBindingsFromDB`` 通过 ``RouteMatchRegistryLoader.loadAll`` 调用 ``repo.listRouteBindings(...)``,需返回可 await 的空列表以跳过 规则加载。 """ stub = MagicMock(spec=PersistencePort) stub.listRouteBindings = AsyncMock(return_value=[]) return stub def _make_bootstrap( fake_logger, *, di_container: DependencyInjectionContainer | None = None, config_port: ConfigPort | None = None, plugin_lifecycle: PluginLifecycleManager | None = None, plugin_registry: PluginRegistry | None = None, route_match_registry: RouteMatchRegistry | None = None, stage_slot_injector: StageSlotInjector | None = None, transport_manager: TransportManager | None = None, driving_adapter_registrar=None, driving_adapter_unregistrar=None, app_driven_adapters=None, ensure_schema: object | None = None, ) -> HostBootstrap: """构造测试用 HostBootstrap,所有依赖默认为 MagicMock。""" if di_container is None: di_container = DependencyInjectionContainer() # 预注册 _loadRouteBindingsFromDB / _registerCommonCommands 所需依赖 di_container.registerSingleton( RouteMatchRegistry, route_match_registry or RouteMatchRegistry() ) di_container.registerSingleton(PersistencePort, _make_persistence_port_stub()) di_container.registerSingleton( PluginRegistry, plugin_registry or MagicMock(spec=PluginRegistry) ) di_container.registerSingleton( PluginLifecycleManager, plugin_lifecycle or MagicMock(spec=PluginLifecycleManager), ) from yuxi.channels.core.registry.command_registry import ( CommandRegistryTable, ) from yuxi.channels.application.lifecycle.sensitive_field_registry import ( SensitiveFieldRegistry, ) from yuxi.channels.application.lifecycle.config_scope_registry import ( ConfigScopeRegistry as LifecycleCSR, ) di_container.registerSingleton(CommandRegistryTable, CommandRegistryTable()) di_container.registerSingleton(SensitiveFieldRegistry, MagicMock()) di_container.registerSingleton(LifecycleCSR, MagicMock()) if config_port is None: config_port = MagicMock(spec=ConfigPort) if ensure_schema is None: ensure_schema = MagicMock() return HostBootstrap( ensure_schema=ensure_schema, di_container=di_container, plugin_dir="/nonexistent/plugins", event_bus=MagicMock(spec=EventBus), stage_slot_registry=MagicMock(spec=StageSlotRegistry), event_subscription_registry=MagicMock(spec=EventSubscriptionRegistry), config_source_registry=MagicMock(spec=ConfigSourceRegistry), plugin_lifecycle_manager=plugin_lifecycle or MagicMock(spec=PluginLifecycleManager), plugin_dependency_resolver=MagicMock(spec=PluginDependencyResolver), stage_slot_injector=stage_slot_injector or MagicMock(spec=StageSlotInjector), health_aggregator=MagicMock(spec=HealthAggregator), transport_manager=transport_manager or MagicMock(spec=TransportManager), logger=fake_logger, config_port=config_port, driving_adapter_registrar=driving_adapter_registrar, driving_adapter_unregistrar=driving_adapter_unregistrar, app_driven_adapters=app_driven_adapters, ) # --------------------------------------------------------------------------- # 入口守卫 # --------------------------------------------------------------------------- @pytest.mark.unit class TestBootstrapEntryGuard: """bootstrap() 入口守卫测试。""" @pytest.mark.asyncio async def test_bootstrap_when_already_ready_raises_internal_error( self, fake_logger ): # Arrange bootstrap = _make_bootstrap(fake_logger) bootstrap._ready = True # Act / Assert with pytest.raises(InternalError) as exc_info: await bootstrap.bootstrap() assert "already bootstrapped" in exc_info.value.message @pytest.mark.asyncio async def test_bootstrap_when_in_progress_raises_internal_error( self, fake_logger ): # Arrange bootstrap = _make_bootstrap(fake_logger) bootstrap._bootstrapping = True # Act / Assert with pytest.raises(InternalError) as exc_info: await bootstrap.bootstrap() assert "in progress" in exc_info.value.message @pytest.mark.asyncio async def test_bootstrap_failure_resets_in_progress_flag(self, fake_logger): """启动失败后 ``_bootstrapping`` 必须重置,允许调用方重试。""" # Arrange config_port = MagicMock(spec=ConfigPort) # _loadConfig 抛 ConfigValidationError 触发失败路径 config_port.getSchema = AsyncMock(side_effect=ConfigValidationError(["boom"])) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act with pytest.raises(ConfigValidationError): await bootstrap.bootstrap() # Assert: 失败后标志重置,ready 仍为 False assert bootstrap._bootstrapping is False assert bootstrap.isReady is False # --------------------------------------------------------------------------- # _loadConfig # --------------------------------------------------------------------------- @pytest.mark.unit class TestLoadConfig: """_loadConfig 配置加载测试。""" @pytest.mark.asyncio async def test_empty_schema_raises_config_validation_error(self, fake_logger): # Arrange config_port = MagicMock(spec=ConfigPort) config_port.getSchema = AsyncMock(return_value=()) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act / Assert with pytest.raises(ConfigValidationError) as exc_info: await bootstrap._loadConfig() assert "empty" in exc_info.value.message.lower() @pytest.mark.asyncio async def test_dependency_error_propagates_unchanged(self, fake_logger): # Arrange config_port = MagicMock(spec=ConfigPort) original_err = DependencyError("Redis", RuntimeError("conn refused")) config_port.getSchema = AsyncMock(side_effect=original_err) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act / Assert with pytest.raises(DependencyError) as exc_info: await bootstrap._loadConfig() assert exc_info.value is original_err @pytest.mark.asyncio async def test_other_exception_wrapped_as_config_validation_error( self, fake_logger ): # Arrange config_port = MagicMock(spec=ConfigPort) config_port.getSchema = AsyncMock(side_effect=RuntimeError("unexpected")) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act / Assert with pytest.raises(ConfigValidationError): await bootstrap._loadConfig() @pytest.mark.asyncio async def test_valid_schema_does_not_raise(self, fake_logger): # Arrange config_port = MagicMock(spec=ConfigPort) config_port.getSchema = AsyncMock(return_value=("field1", "field2")) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act / Assert: 不抛异常即成功 await bootstrap._loadConfig() # --------------------------------------------------------------------------- # _initSchema # --------------------------------------------------------------------------- @pytest.mark.unit class TestInitSchema: """_initSchema schema 初始化测试。""" @pytest.mark.asyncio async def test_failure_propagates_exception(self, fake_logger): # Arrange ensure_schema = MagicMock() ensure_schema.ensure_channel_schema = AsyncMock( side_effect=RuntimeError("db down") ) bootstrap = _make_bootstrap(fake_logger, ensure_schema=ensure_schema) # Act / Assert with pytest.raises(RuntimeError, match="db down"): await bootstrap._initSchema() @pytest.mark.asyncio async def test_success_does_not_raise(self, fake_logger): # Arrange ensure_schema = MagicMock() ensure_schema.ensure_channel_schema = AsyncMock(return_value=None) bootstrap = _make_bootstrap(fake_logger, ensure_schema=ensure_schema) # Act / Assert await bootstrap._initSchema() # --------------------------------------------------------------------------- # _initDrivenAdapters / _pingAdapter # --------------------------------------------------------------------------- @pytest.mark.unit class TestInitDrivenAdapters: """_initDrivenAdapters / _pingAdapter 连通性检查测试。""" @pytest.mark.asyncio async def test_empty_adapters_does_not_raise(self, fake_logger): # Arrange bootstrap = _make_bootstrap(fake_logger, app_driven_adapters=()) # Act / Assert: 空列表直接返回(向后兼容) await bootstrap._initDrivenAdapters() @pytest.mark.asyncio async def test_none_adapters_does_not_raise(self, fake_logger): # Arrange bootstrap = _make_bootstrap(fake_logger, app_driven_adapters=None) # Act / Assert await bootstrap._initDrivenAdapters() @pytest.mark.asyncio async def test_non_pingable_adapter_skipped(self, fake_logger): """未实现 PingablePort 契约的适配器显式跳过。""" # Arrange non_pingable = MagicMock() # 不实现 PingablePort bootstrap = _make_bootstrap( fake_logger, app_driven_adapters=[non_pingable] ) # Act / Assert: 不抛异常即证明跳过逻辑生效 await bootstrap._initDrivenAdapters() @pytest.mark.asyncio async def test_ping_returns_false_raises_dependency_error(self, fake_logger): # Arrange pingable_adapter = MagicMock(spec=PingablePort) pingable_adapter.ping = AsyncMock(return_value=False) # spec=PingablePort 让 isinstance 检查通过 bootstrap = _make_bootstrap( fake_logger, app_driven_adapters=[pingable_adapter] ) # Act / Assert with pytest.raises(DependencyError): await bootstrap._initDrivenAdapters() @pytest.mark.asyncio async def test_ping_raises_exception_wrapped_as_dependency_error( self, fake_logger ): # Arrange pingable_adapter = MagicMock(spec=PingablePort) pingable_adapter.ping = AsyncMock(side_effect=RuntimeError("conn lost")) bootstrap = _make_bootstrap( fake_logger, app_driven_adapters=[pingable_adapter] ) # Act / Assert with pytest.raises(DependencyError): await bootstrap._initDrivenAdapters() @pytest.mark.asyncio async def test_ping_returns_true_passes(self, fake_logger): # Arrange pingable_adapter = MagicMock(spec=PingablePort) pingable_adapter.ping = AsyncMock(return_value=True) bootstrap = _make_bootstrap( fake_logger, app_driven_adapters=[pingable_adapter] ) # Act / Assert await bootstrap._initDrivenAdapters() # --------------------------------------------------------------------------- # _loadPlugins / _loadSinglePlugin # --------------------------------------------------------------------------- @pytest.mark.unit class TestLoadPlugins: """_loadPlugins 插件加载测试。""" @pytest.mark.asyncio async def test_discover_failure_warns_and_returns(self, fake_logger): """discover 抛异常时记录告警并跳过插件加载(优雅降级)。""" # Arrange plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.discover = AsyncMock(side_effect=RuntimeError("disk error")) bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) # _loadPlugins 需要 resolve SensitiveFieldRegistry / ConfigScopeRegistry # 已在 _make_bootstrap 中预注册 # Act: 不抛异常即证明 warn-and-continue await bootstrap._loadPlugins() # Assert: 插件未加载(_plugins_loaded 仍为 False,因 discover 失败提前 return) assert bootstrap._plugins_loaded is False # 告警日志被调用 fake_logger.warn.assert_called() @pytest.mark.asyncio async def test_empty_manifests_returns_early(self, fake_logger): # Arrange plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.discover = AsyncMock(return_value=[]) bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) # Act await bootstrap._loadPlugins() # Assert: 空 manifest 提前返回,_plugins_loaded 仍为 False assert bootstrap._plugins_loaded is False @pytest.mark.asyncio async def test_single_plugin_load_failure_warns_and_continues( self, fake_logger ): """单个插件 load 失败记录告警并继续,不阻塞宿主启动。""" # Arrange manifest = _make_manifest("failed-plugin") plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.discover = AsyncMock(return_value=[manifest]) plugin_lifecycle.load = AsyncMock( return_value=LifecycleResult( state="failed", error="init error" ) ) # resolver.resolve 返回原列表(无依赖排序) resolver = MagicMock(spec=PluginDependencyResolver) resolver.resolve = MagicMock(return_value=[manifest]) bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) bootstrap._resolver = resolver # Act await bootstrap._loadPlugins() # Assert: _plugins_loaded 置位(步骤完成,含优雅降级) assert bootstrap._plugins_loaded is True # load 被调用一次 plugin_lifecycle.load.assert_awaited_once_with("failed-plugin") # 告警日志记录失败 fake_logger.warn.assert_called() @pytest.mark.asyncio async def test_single_plugin_load_success_logs_info(self, fake_logger): # Arrange manifest = _make_manifest("ok-plugin") plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.discover = AsyncMock(return_value=[manifest]) plugin_lifecycle.load = AsyncMock( return_value=LifecycleResult(state="started") ) resolver = MagicMock(spec=PluginDependencyResolver) resolver.resolve = MagicMock(return_value=[manifest]) bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) bootstrap._resolver = resolver # Act await bootstrap._loadPlugins() # Assert assert bootstrap._plugins_loaded is True fake_logger.info.assert_called() # --------------------------------------------------------------------------- # _rollback # --------------------------------------------------------------------------- @pytest.mark.unit class TestRollback: """_rollback 逆序回滚测试。""" @pytest.mark.asyncio async def test_calls_driving_unregistrar_when_registered(self, fake_logger): """_driving_registered=True 且提供 unregistrar 时调用注销回调。""" # Arrange unregistrar = AsyncMock() bootstrap = _make_bootstrap( fake_logger, driving_adapter_unregistrar=unregistrar ) bootstrap._driving_registered = True # Act await bootstrap._rollback() # Assert unregistrar.assert_awaited_once() @pytest.mark.asyncio async def test_skips_driving_unregistrar_when_not_registered(self, fake_logger): """_driving_registered=False 时不调用注销回调。""" # Arrange unregistrar = AsyncMock() bootstrap = _make_bootstrap( fake_logger, driving_adapter_unregistrar=unregistrar ) bootstrap._driving_registered = False # Act await bootstrap._rollback() # Assert unregistrar.assert_not_awaited() @pytest.mark.asyncio async def test_skips_driving_unregistrar_when_none(self, fake_logger): """driving_adapter_unregistrar=None 时跳过注销步骤。""" # Arrange bootstrap = _make_bootstrap(fake_logger, driving_adapter_unregistrar=None) bootstrap._driving_registered = True # Act / Assert: 不抛异常即证明 None 路径安全 await bootstrap._rollback() @pytest.mark.asyncio async def test_stops_plugins_when_loaded(self, fake_logger): """_plugins_loaded=True 时调用 stopAllStartedPlugins。""" # Arrange plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.stopAllStartedPlugins = AsyncMock() bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) bootstrap._plugins_loaded = True # Act await bootstrap._rollback() # Assert plugin_lifecycle.stopAllStartedPlugins.assert_awaited_once() @pytest.mark.asyncio async def test_skips_stop_plugins_when_not_loaded(self, fake_logger): # Arrange plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.stopAllStartedPlugins = AsyncMock() bootstrap = _make_bootstrap(fake_logger, plugin_lifecycle=plugin_lifecycle) bootstrap._plugins_loaded = False # Act await bootstrap._rollback() # Assert plugin_lifecycle.stopAllStartedPlugins.assert_not_awaited() @pytest.mark.asyncio async def test_driving_unregistrar_failure_does_not_block_subsequent_steps( self, fake_logger ): """注销回调抛异常时记录告警并继续后续回滚步骤(INV-9)。""" # Arrange unregistrar = AsyncMock(side_effect=RuntimeError("unreg failed")) plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.stopAllStartedPlugins = AsyncMock() bootstrap = _make_bootstrap( fake_logger, plugin_lifecycle=plugin_lifecycle, driving_adapter_unregistrar=unregistrar, ) bootstrap._driving_registered = True bootstrap._plugins_loaded = True # Act: 不抛异常即证明后续步骤继续执行 await bootstrap._rollback() # Assert: 后续 stopAllStartedPlugins 仍被调用 unregistrar.assert_awaited_once() plugin_lifecycle.stopAllStartedPlugins.assert_awaited_once() fake_logger.error.assert_called() @pytest.mark.asyncio async def test_cancels_background_tasks(self, fake_logger): """回滚时取消已启动的后台任务。""" # Arrange bootstrap = _make_bootstrap(fake_logger) task1 = asyncio.create_task(asyncio.sleep(100)) task2 = asyncio.create_task(asyncio.sleep(100)) bootstrap._plugin_reload_task = task1 bootstrap._transport_manager_task = task2 # Act await bootstrap._rollback() # _cancelBackgroundTasks 仅调度取消(同步),需让事件循环处理取消完成 await asyncio.sleep(0) # Assert: 任务被取消 assert task1.cancelled() or task1.done() assert task2.cancelled() or task2.done() # --------------------------------------------------------------------------- # bootstrap 端到端(happy path) # --------------------------------------------------------------------------- @pytest.mark.unit class TestBootstrapHappyPath: """bootstrap() 成功路径测试。""" @pytest.mark.asyncio async def test_full_bootstrap_marks_ready(self, fake_logger): """成功路径:所有步骤完成 → isReady=True。""" # Arrange: 构造所有依赖的桩,配置为成功路径 config_port = MagicMock(spec=ConfigPort) config_port.getSchema = AsyncMock(return_value=("field1",)) ensure_schema = MagicMock() ensure_schema.ensure_channel_schema = AsyncMock(return_value=None) plugin_lifecycle = MagicMock(spec=PluginLifecycleManager) plugin_lifecycle.discover = AsyncMock(return_value=[]) # reloadLoop 返回的协程需长时间挂起以模拟后台任务 plugin_lifecycle.reloadLoop = MagicMock(return_value=self._endless_coro()) transport_manager = MagicMock(spec=TransportManager) transport_manager.start = MagicMock(return_value=self._endless_coro()) stage_slot_injector = MagicMock(spec=StageSlotInjector) stage_slot_injector.validateAnchors = MagicMock() bootstrap = _make_bootstrap( fake_logger, config_port=config_port, plugin_lifecycle=plugin_lifecycle, stage_slot_injector=stage_slot_injector, transport_manager=transport_manager, app_driven_adapters=(), ) bootstrap._ensure_schema = ensure_schema # Act await bootstrap.bootstrap() # Assert assert bootstrap.isReady is True # 后台任务引用已填充 assert bootstrap.pluginReloadTask is not None assert bootstrap.transportManagerTask is not None # 清理:取消后台任务避免泄漏到后续测试 bootstrap.pluginReloadTask.cancel() bootstrap.transportManagerTask.cancel() try: await bootstrap.pluginReloadTask except (asyncio.CancelledError, Exception): pass try: await bootstrap.transportManagerTask except (asyncio.CancelledError, Exception): pass @staticmethod def _endless_coro(): """构造一个长时间挂起的协程,模拟后台任务。""" return asyncio.sleep(1000) @pytest.mark.asyncio async def test_bootstrap_failure_triggers_rollback(self, fake_logger): """关键步骤失败时触发 _rollback,异常向上抛出。""" # Arrange: 让 _loadConfig 抛 ConfigValidationError config_port = MagicMock(spec=ConfigPort) config_port.getSchema = AsyncMock( side_effect=ConfigValidationError(["bad config"]) ) bootstrap = _make_bootstrap(fake_logger, config_port=config_port) # Act / Assert with pytest.raises(ConfigValidationError): await bootstrap.bootstrap() # 失败后 ready 仍为 False assert bootstrap.isReady is False # --------------------------------------------------------------------------- # 后台任务启动探测 # --------------------------------------------------------------------------- @pytest.mark.unit class TestStartBackgroundTask: """_startBackgroundTask 启动即失败探测测试。""" @pytest.mark.asyncio async def test_task_immediate_failure_raises_dependency_error( self, fake_logger ): # Arrange bootstrap = _make_bootstrap(fake_logger) async def _fail_immediately() -> None: raise RuntimeError("startup boom") # Act / Assert: 任务在探测窗口内失败,包装为 DependencyError with pytest.raises(DependencyError): await bootstrap._startBackgroundTask( _fail_immediately(), name="test_task", log_message="should not see this", ) @pytest.mark.asyncio async def test_long_running_task_returns_successfully(self, fake_logger): # Arrange bootstrap = _make_bootstrap(fake_logger) async def _long_running() -> None: await asyncio.sleep(100) # Act task = await bootstrap._startBackgroundTask( _long_running(), name="test_task", log_message="task started", ) # Assert assert not task.done() task.cancel() try: await task except (asyncio.CancelledError, Exception): pass # --------------------------------------------------------------------------- # isReady / pluginReloadTask / transportManagerTask 属性 # --------------------------------------------------------------------------- @pytest.mark.unit class TestProperties: """isReady / pluginReloadTask / transportManagerTask 属性测试。""" def test_is_ready_default_false(self, fake_logger): bootstrap = _make_bootstrap(fake_logger) assert bootstrap.isReady is False def test_plugin_reload_task_default_none(self, fake_logger): bootstrap = _make_bootstrap(fake_logger) assert bootstrap.pluginReloadTask is None def test_transport_manager_task_default_none(self, fake_logger): bootstrap = _make_bootstrap(fake_logger) assert bootstrap.transportManagerTask is None