ConfigManager V2

This commit is contained in:
2026-02-19 22:45:06 +03:00
parent da8ed4fa2b
commit 7eb3476b96
17 changed files with 801 additions and 2 deletions

View File

@@ -0,0 +1,33 @@
import asyncio
from config_manager.v2 import ConfigManagerV2
class ReloadApp(ConfigManagerV2):
def execute(self) -> None:
return
def test_invalid_config_keeps_last_valid(tmp_path):
async def scenario() -> None:
cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.2\nupdate_interval: 0.05\n", encoding="utf-8")
app = ReloadApp(str(cfg))
runner = asyncio.create_task(app.start())
await asyncio.sleep(0.12)
assert app.work_interval == 0.2
assert app.update_interval == 0.05
cfg.write_text("work_interval: [broken\n", encoding="utf-8")
await asyncio.sleep(0.15)
assert app.work_interval == 0.2
assert app.update_interval == 0.05
assert isinstance(app.config, dict)
await app.stop()
await runner
asyncio.run(scenario())

View File

@@ -0,0 +1,30 @@
import asyncio
from config_manager.v2 import ConfigManagerV2
class DemoApp(ConfigManagerV2):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.calls = 0
def execute(self) -> None:
self.calls += 1
def test_execute_loop_runs(tmp_path):
async def scenario() -> None:
cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8")
app = DemoApp(str(cfg))
runner = asyncio.create_task(app.start())
await asyncio.sleep(0.18)
await app.stop()
await runner
assert app.calls >= 2
assert isinstance(app.config, dict)
asyncio.run(scenario())

View File

@@ -0,0 +1,54 @@
import asyncio
from config_manager.v2 import ConfigManagerV2
from config_manager.v2.control.base import ControlChannel, StartHandler, StatusHandler, StopHandler
class DummyControlChannel(ControlChannel):
def __init__(self):
self.on_start: StartHandler | None = None
self.on_stop: StopHandler | None = None
self.on_status: StatusHandler | None = None
self.started = False
self.stopped = False
async def start(self, on_start: StartHandler, on_stop: StopHandler, on_status: StatusHandler) -> None:
self.on_start = on_start
self.on_stop = on_stop
self.on_status = on_status
self.started = True
async def stop(self) -> None:
self.stopped = True
class ControlledApp(ConfigManagerV2):
def execute(self) -> None:
return
def test_control_channel_can_stop_manager(tmp_path):
async def scenario() -> None:
cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8")
channel = DummyControlChannel()
app = ControlledApp(str(cfg), control_channel=channel)
runner = asyncio.create_task(app.start())
await asyncio.sleep(0.12)
assert channel.started is True
assert channel.on_status is not None
assert channel.on_stop is not None
status_text = await channel.on_status()
assert "state=running" in status_text
stop_text = await channel.on_stop()
assert "stop signal accepted" in stop_text
await runner
assert channel.stopped is True
asyncio.run(scenario())

View File

@@ -0,0 +1,41 @@
import asyncio
from config_manager.v2.health import HealthServer
def test_health_mapping_ok_to_200():
async def provider():
return {"status": "ok"}
async def scenario() -> None:
server = HealthServer(
host="127.0.0.1",
port=8000,
path="/health",
timeout=0.2,
health_provider=provider,
)
code, payload = await server._build_health_response()
assert code == 200
assert payload["status"] == "ok"
asyncio.run(scenario())
def test_health_mapping_unhealthy_to_503():
async def provider():
return {"status": "unhealthy", "detail": "worker failed"}
async def scenario() -> None:
server = HealthServer(
host="127.0.0.1",
port=8000,
path="/health",
timeout=0.2,
health_provider=provider,
)
code, payload = await server._build_health_response()
assert code == 503
assert payload["status"] == "unhealthy"
asyncio.run(scenario())

View File

@@ -0,0 +1,43 @@
import asyncio
import threading
import time
from config_manager.v2 import ConfigManagerV2
class BlockingApp(ConfigManagerV2):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.started_event = threading.Event()
self.active_event = threading.Event()
self.calls = 0
def execute(self) -> None:
self.calls += 1
self.active_event.set()
self.started_event.set()
time.sleep(0.2)
self.active_event.clear()
def test_stop_waits_for_active_execute_and_prevents_next_run(tmp_path):
async def scenario() -> None:
cfg = tmp_path / "config.yaml"
cfg.write_text("work_interval: 0.05\nupdate_interval: 0.05\n", encoding="utf-8")
app = BlockingApp(str(cfg))
runner = asyncio.create_task(app.start())
started = await asyncio.to_thread(app.started_event.wait, 1.0)
assert started is True
await app.stop()
await runner
assert app.active_event.is_set() is False
assert app.calls == 1
await asyncio.sleep(0.15)
assert app.calls == 1
asyncio.run(scenario())