mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-11-13 09:13:30 +01:00
fix(config): enable documentation mode only explicitly
ConfigEOS.documentation_mode() was true as soon as Sphinx was imported anywhere in the process. A test importing Sphinx therefore switched all later tests into documentation mode, and test_config_md_current failed depending on test order. Documentation mode is now enabled only by the documentation generators (_force_documentation_mode) or by the Sphinx configuration, which sets sys._called_from_sphinx in docs/conf.py. Declare _force_documentation_mode as a ClassVar. Without the annotation pydantic turned it into a private attribute, and the class attribute was a truthy descriptor object. Behavior change: because of that descriptor, every running EOS was in documentation mode, so the get_adapter/config/measurement/prediction/ems/ database/resource_registry() guards never raised "access before init" and created the singletons lazily on first access. They now raise for every access before initialization. The worker process of uvicorn's auto-reload (server.reload, make run-dev) relied on that: the app's lifespan now sets up the configuration and initializes the singletons that do not exist yet. Leave runtime state out of the documentation: the Home Assistant entity ID properties and OptimizationCommonSettings.keys return an empty list in documentation mode, so entity IDs and solution keys of a running system no longer reach the generated configuration docs. Keep the tests isolated: test_doc sets documentation mode on the class and test_docstringrst restores sys._called_from_sphinx after reading the Sphinx configuration. Add tests for how documentation mode is enabled and for the runtime values it leaves out. Additional changes - Generate openapi.json in documentation mode, as the config docs. - Share the documentation mode check as coreabc.documentation_mode(). - Move the logging and server address setup of run_eos() to setup_eos_config(). - Test that EOS starts with server.reload. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a0e57a18b7
commit
57132df44e
@@ -2,6 +2,9 @@ import asyncio
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
@@ -395,6 +398,114 @@ class TestServerWithEnv:
|
||||
)
|
||||
|
||||
|
||||
class TestServerReload:
|
||||
"""EOS with uvicorn's auto-reload (``server.reload``, used by ``make run-dev``)."""
|
||||
|
||||
STARTUP_TIMEOUT_S = 90
|
||||
|
||||
@staticmethod
|
||||
def free_port() -> int:
|
||||
"""Return a free TCP port on the default host that EOS accepts (1024..49151).
|
||||
|
||||
Starts above the ports of the other server tests (8503, 8504, 8555).
|
||||
"""
|
||||
for port in range(8513, 8600):
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
sock.bind((get_default_host(), port))
|
||||
except OSError:
|
||||
continue
|
||||
return port
|
||||
pytest.fail("No free port for the EOS reload test in 8513..8599")
|
||||
|
||||
@staticmethod
|
||||
def stop_process_tree(process: subprocess.Popen) -> None:
|
||||
"""Stop a process and all of its descendants (reloader, worker)."""
|
||||
try:
|
||||
parent = psutil.Process(process.pid)
|
||||
processes = [*parent.children(recursive=True), parent]
|
||||
except psutil.NoSuchProcess:
|
||||
return
|
||||
for proc in processes:
|
||||
try:
|
||||
proc.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
_, alive = psutil.wait_procs(processes, timeout=15)
|
||||
for proc in alive:
|
||||
try:
|
||||
proc.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
process.wait(timeout=15)
|
||||
|
||||
def test_server_starts_with_reload(self, tmp_path: Path) -> None:
|
||||
"""The reload worker initializes the singletons that run_eos() set up in the parent.
|
||||
|
||||
The worker process imports the app afresh. Without initialization in the
|
||||
app's lifespan, the startup fails with 'access before init'.
|
||||
"""
|
||||
eos_dir = tmp_path / "eos"
|
||||
eos_dir.mkdir()
|
||||
# An empty config file, so EOS does not fall back to the user's config file.
|
||||
(eos_dir / "EOS.config.json").write_text("{}", encoding="utf-8")
|
||||
host = get_default_host()
|
||||
port = self.free_port()
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"EOS_DIR": str(eos_dir),
|
||||
"EOS_CONFIG_DIR": str(eos_dir),
|
||||
"PYTHONIOENCODING": "utf-8",
|
||||
}
|
||||
)
|
||||
log_path = tmp_path / "eos.log"
|
||||
with log_path.open("w", encoding="utf-8") as log_file:
|
||||
# cwd: the reloader watches the working directory, not the repository.
|
||||
process = subprocess.Popen(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"akkudoktoreos.server.eos",
|
||||
"--host",
|
||||
host,
|
||||
"--port",
|
||||
str(port),
|
||||
"--reload",
|
||||
"true",
|
||||
"--startup_eosdash",
|
||||
"false",
|
||||
],
|
||||
cwd=tmp_path,
|
||||
env=env,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
try:
|
||||
health: dict = {}
|
||||
deadline = time.monotonic() + self.STARTUP_TIMEOUT_S
|
||||
while time.monotonic() < deadline and process.poll() is None:
|
||||
try:
|
||||
response = requests.get(f"http://{host}:{port}/v1/health", timeout=2)
|
||||
if response.status_code == HTTPStatus.OK:
|
||||
health = response.json()
|
||||
break
|
||||
except requests.RequestException:
|
||||
pass
|
||||
time.sleep(1)
|
||||
worker_pids = {
|
||||
child.pid for child in psutil.Process(process.pid).children(recursive=True)
|
||||
} if process.poll() is None else set()
|
||||
finally:
|
||||
self.stop_process_tree(process)
|
||||
|
||||
log = log_path.read_text(encoding="utf-8", errors="replace")
|
||||
assert health.get("status") == "alive", f"EOS with --reload did not start:\n{log}"
|
||||
assert "before init" not in log, log
|
||||
# The app runs in the reload worker, a child of the started process.
|
||||
assert health.get("pid") in worker_pids
|
||||
|
||||
|
||||
class TestEosdashRedirect:
|
||||
"""Redirects to EOSdash must target an address the client can reach.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user