Files
EOS/tests/test_optimization_pdf_api.py
T
3651d33915 perf: import heavy optional dependencies on first use (#1377)
A GENETIC server with external (import) providers and EOSdash disabled
loads several large packages at start that it never uses, which costs
a noticeable part of the memory on 512 MB boards:

- matplotlib via the GENETIC/GENETIC0 PDF report modules (server/eos.py)
- statsmodels via the Holt-Winters fallback in prediction/priceabc.py
- pvlib via the weather base class and the pvlib-based providers
- bs4/lxml via the ClearOutside provider
- scipy via the self-consumption interpolator singletons, which were
  loaded at import time
- the GENETIC0 optimizer, imported by core/ems.py
- tzfpy, whose polygon data the computed general.timezone loads

Import these where they are used. The interpolator tables are loaded
on first use (thread-safe). Nothing changes in what is computed.

general.timezone is computed from latitude/longitude and cannot be set.
Add general.timezone_override (IANA name, validated with pendulum):
when set, general.timezone returns it and the lookup is skipped. When
unset (default) the lookup is unchanged.

Tests that patched the moved names now patch them at their source.
A new test starts the server module in a subprocess and checks that
none of these modules is loaded.

Co-authored-by: Christin <christin@BigBrother.bikini.bottom.zone>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 08:40:43 +02:00

81 lines
3.3 KiB
Python

"""The report endpoint selects retained results and renders outside the event loop."""
import threading
from io import BytesIO
from pathlib import Path
from types import SimpleNamespace
import pytest
from fastapi.testclient import TestClient
from pypdf import PdfReader
from akkudoktoreos.optimization.genetic import geneticvisualize
from akkudoktoreos.optimization.genetic.geneticsolution import GeneticSolution
from akkudoktoreos.server import eos
from akkudoktoreos.utils.datetimeutil import to_datetime
@pytest.fixture
def retained_result():
path = Path(__file__).parent / "testdata/genetic/optimize_result_1.json"
solution = GeneticSolution.model_validate_json(path.read_text())
solution.start_solution_datetime = to_datetime("2026-09-16T10:00:00+02:00")
return solution
def test_missing_algorithm_report_returns_404(config_eos, monkeypatch):
monkeypatch.setattr(eos, "get_ems", lambda: SimpleNamespace(genetic_solution=lambda: None))
response = TestClient(eos.app).get("/v1/energy-management/optimization/solution/GENETIC/pdf")
assert response.status_code == 404
assert response.headers["content-type"].startswith("application/problem+json")
def test_retained_algorithm_result_renders_real_pdf(config_eos, monkeypatch, retained_result):
solution = retained_result
monkeypatch.setattr(eos, "get_ems", lambda: SimpleNamespace(genetic_solution=lambda: solution))
# A later run and changed configuration must not retime the retained report.
from akkudoktoreos.core.coreabc import get_ems
from akkudoktoreos.optimization.genetic import geneticvisualize
get_ems().set_start_datetime(to_datetime("2027-01-01T00:00:00+01:00"))
config_eos.optimization.genetic.interval_sec = 900
monkeypatch.setattr(
geneticvisualize, "get_ems", lambda: pytest.fail("Report must retain its own time grid")
)
before = solution.model_dump_json()
response = TestClient(eos.app).get("/v1/energy-management/optimization/solution/GENETIC/pdf")
assert response.status_code == 200
assert response.headers["content-type"] == "application/pdf"
assert "genetic" in response.headers["content-disposition"]
assert response.content.startswith(b"%PDF-")
assert len(PdfReader(BytesIO(response.content)).pages) >= 4
assert solution.model_dump_json() == before
@pytest.mark.asyncio
async def test_render_is_offloaded_and_uses_an_owned_copy(config_eos, monkeypatch, retained_result):
solution = retained_result
caller_thread = threading.get_ident()
captured = []
monkeypatch.setattr(
eos,
"get_ems",
lambda: SimpleNamespace(
genetic_solution=lambda: solution, genetic0_solution=lambda: solution
),
)
original_controls = list(solution.ac_charge)
def render(*, solution):
captured.append((threading.get_ident(), solution))
solution.ac_charge[0] = 0.123
return b"%PDF-isolated-render"
# The PDF stack is imported on first use, so patch it at its source.
monkeypatch.setattr(geneticvisualize, "genetic_prepare_visualize", render)
response = await eos.fastapi_energy_management_optimization_solution_genetic_pdf_get()
assert response.body == b"%PDF-isolated-render"
assert captured[0][0] != caller_thread
assert captured[0][1] is not solution
assert solution.ac_charge == original_controls