"""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