8431aa7095
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
165 lines
7.0 KiB
Python
165 lines
7.0 KiB
Python
"""Report PDF veri su Windows (WeasyPrint + DLL GTK + Kaleido con Edge/Chrome), offline.
|
|
|
|
Pacchetto Windows (TieMeasureFlow#78): senza Internet i report devono uscire lo stesso.
|
|
Il test gira solo su Windows, con le DLL di packaging/windows/pdf/prendi-gtk.ps1 e un browser
|
|
Chromium (Edge o Chrome) installati; altrimenti viene saltato.
|
|
|
|
Il conftest dei test sostituisce WeasyPrint con un mock: qui serve quello vero, quindi la
|
|
generazione gira in un sottoprocesso che importa weasyprint PRIMA del conftest (il mock scatta
|
|
solo se weasyprint non e' gia' in sys.modules).
|
|
|
|
Come preparare le DLL: powershell -File packaging/windows/pdf/prendi-gtk.ps1 -Destinazione packaging/windows/pdf/gtk
|
|
(oppure WEASYPRINT_DLL_DIRECTORIES gia' impostata su una cartella con le DLL).
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO = Path(__file__).resolve().parents[3]
|
|
GTK_DEFAULT = REPO / "packaging" / "windows" / "pdf" / "gtk"
|
|
|
|
# Browser Chromium cercati in ordine (Edge e' presente su ogni Windows 10/11).
|
|
_BROWSER_CANDIDATI = [
|
|
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
|
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
|
|
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
]
|
|
|
|
|
|
def _cartella_gtk() -> Path | None:
|
|
"""Cartella DLL: WEASYPRINT_DLL_DIRECTORIES (primo elemento) o quella di prendi-gtk.ps1."""
|
|
env = os.environ.get("WEASYPRINT_DLL_DIRECTORIES", "").split(";")[0]
|
|
for c in (Path(env) if env else None, GTK_DEFAULT):
|
|
if c and (c / "libpango-1.0-0.dll").exists():
|
|
return c
|
|
return None
|
|
|
|
|
|
def _browser() -> str | None:
|
|
env = os.environ.get("BROWSER_PATH")
|
|
if env and Path(env).exists():
|
|
return env
|
|
return next((b for b in _BROWSER_CANDIDATI if Path(b).exists()), None)
|
|
|
|
|
|
def _tz_ok() -> bool:
|
|
"""Windows non ha il database IANA: serve il pacchetto tzdata (to_local usa ZoneInfo)."""
|
|
try:
|
|
from zoneinfo import ZoneInfo
|
|
|
|
ZoneInfo("Europe/Rome")
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
pytestmark = [
|
|
pytest.mark.skipif(sys.platform != "win32", reason="solo Windows"),
|
|
pytest.mark.skipif(_cartella_gtk() is None, reason="DLL GTK assenti: lancia prendi-gtk.ps1"),
|
|
pytest.mark.skipif(_browser() is None, reason="nessun Edge/Chrome installato"),
|
|
# Senza tzdata i report falliscono su Windows (ZoneInfoNotFoundError): il pacchetto deve dichiararlo.
|
|
pytest.mark.skipif(not _tz_ok(), reason="manca tzdata: aggiungere 'tzdata; sys_platform == \"win32\"' a pyproject"),
|
|
]
|
|
|
|
# Sottoprocesso: dati veri nel DB SQLite in memoria del conftest, poi i due report reali.
|
|
_WORKER = r"""
|
|
import asyncio, json, sys
|
|
sys.path.insert(0, sys.argv[1])
|
|
import weasyprint # VERO, prima del conftest (che altrimenti lo sostituisce con un mock)
|
|
assert not type(weasyprint.HTML).__module__.startswith("unittest"), "WeasyPrint mockato"
|
|
from datetime import datetime, timedelta
|
|
from src.backend.tests.conftest import Base, TestSessionFactory, test_engine, _create_user, create_test_recipe
|
|
from src.backend.models.orm.measurement import Measurement
|
|
from src.backend.models.orm.task import RecipeSubtask
|
|
from src.backend.services import report_service
|
|
from sqlalchemy import select
|
|
|
|
VALORI = [10.02, 9.98, 10.05, 10.11, 9.94, 10.0, 10.07, 9.96, 10.03, 10.09, 9.99, 10.04, 10.38, 10.01, 9.97]
|
|
|
|
async def main():
|
|
async with test_engine.begin() as conn:
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
async with TestSessionFactory() as db:
|
|
user = await _create_user(db, "metrologo", roles=["Metrologist"])
|
|
recipe = await create_test_recipe(db, user.id)
|
|
version = recipe.versions[0]
|
|
sub = (await db.execute(select(RecipeSubtask))).scalars().first()
|
|
t0 = datetime.utcnow() - timedelta(hours=1)
|
|
for i, v in enumerate(VALORI):
|
|
pf = "warning" if v > 10.3 else "pass"
|
|
db.add(Measurement(subtask_id=sub.id, version_id=version.id, measured_by=user.id,
|
|
value=v, pass_fail=pf, measured_at=t0 + timedelta(minutes=i)))
|
|
await db.commit()
|
|
spc = await report_service.generate_spc_report(db, recipe.id, sub.id)
|
|
mis = await report_service.generate_measurement_report(db, recipe.id)
|
|
open(sys.argv[2] + "/spc.pdf", "wb").write(spc)
|
|
open(sys.argv[2] + "/misure.pdf", "wb").write(mis)
|
|
# MathJax locale: senza, Kaleido chiede lo script al CDN e senza Internet ogni grafico attende ~19 s.
|
|
from pathlib import Path
|
|
mj = report_service.pio.defaults.mathjax
|
|
print(json.dumps({"ok": True, "mathjax_locale": bool(mj) and Path(mj).is_file()}))
|
|
|
|
asyncio.run(main())
|
|
"""
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pdf_generati(tmp_path_factory):
|
|
"""Genera i due PDF in un sottoprocesso con proxy finto: se serve Internet, fallisce."""
|
|
out = tmp_path_factory.mktemp("report_win")
|
|
env = dict(os.environ)
|
|
env.update(
|
|
WEASYPRINT_DLL_DIRECTORIES=str(_cartella_gtk()),
|
|
BROWSER_PATH=_browser(),
|
|
# Proxy morto per la parte Python (Chrome/Edge usano invece il proxy di sistema: vedi LEGGIMI.md).
|
|
HTTP_PROXY="http://127.0.0.1:9", HTTPS_PROXY="http://127.0.0.1:9",
|
|
PYTHONIOENCODING="utf-8",
|
|
)
|
|
# Il sottoprocesso non deve leggere il .env di sviluppo ne' un DB reale.
|
|
env.pop("DB_URL", None)
|
|
res = subprocess.run(
|
|
[sys.executable, "-W", "ignore", "-c", _WORKER, str(REPO), str(out)],
|
|
cwd=REPO, env=env, capture_output=True, text=True, timeout=180,
|
|
)
|
|
assert res.returncode == 0, f"sottoprocesso fallito:\n{res.stdout}\n{res.stderr}"
|
|
esito = json.loads(res.stdout.strip().splitlines()[-1])
|
|
assert esito["ok"] is True
|
|
assert esito["mathjax_locale"], "Kaleido userebbe MathJax dal CDN: niente rete = report lenti"
|
|
return out
|
|
|
|
|
|
def _pdf(path: Path):
|
|
import pdfplumber
|
|
|
|
return pdfplumber.open(path)
|
|
|
|
|
|
def test_spc_ha_carta_di_controllo_e_istogramma(pdf_generati):
|
|
"""Il report SPC e' un PDF vero con una pagina almeno e i due grafici disegnati."""
|
|
path = pdf_generati / "spc.pdf"
|
|
assert path.read_bytes()[:5] == b"%PDF-"
|
|
with _pdf(path) as pdf:
|
|
assert len(pdf.pages) >= 1
|
|
testo = "\n".join(p.extract_text() or "" for p in pdf.pages)
|
|
# I titoli stanno dentro l'SVG di Plotly: se ci sono, Kaleido ha reso e WeasyPrint ha disegnato.
|
|
assert "Control Chart" in testo
|
|
assert "Histogram" in testo
|
|
# Grafico presente = segni vettoriali (tracce, assi, barre) oltre al testo.
|
|
segni = sum(len(p.curves) + len(p.lines) + len(p.rects) for p in pdf.pages)
|
|
assert segni > 40, f"troppo pochi segni vettoriali ({segni}): grafico assente?"
|
|
|
|
|
|
def test_report_misure_e_pdf_vero(pdf_generati):
|
|
"""Anche il report a tabella (solo WeasyPrint, niente Kaleido) esce con le righe misurate."""
|
|
path = pdf_generati / "misure.pdf"
|
|
assert path.read_bytes()[:5] == b"%PDF-"
|
|
with _pdf(path) as pdf:
|
|
assert len(pdf.pages) >= 1
|
|
testo = "\n".join(p.extract_text() or "" for p in pdf.pages)
|
|
assert "10.3800" in testo # il valore anomalo e' in tabella
|