"""Demo end-to-end del nuovo scheduler_locale.py + run_job.py — verifica lo schema esteso di schedule.json e il ciclo fire-and-forget completo, senza toccare nave/schedule.json ne' i job reali in produzione. Costruisce un ambiente di test a se' (schedule.json temporaneo, job .bat fittizio veloce, copia locale di run_job.py) sotto demo/_scratch/, lo ripulisce a ogni run. Monkeypatcha scheduler_locale.UFFICIO_ROOT/LOG_DIR per la durata del test, cosi' dispatch_job() risolve i comandi e scrive i log dentro lo scratch invece che nella ufficio/ reale. """ import json import shutil import sys import time from datetime import datetime from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE.parent / "ufficio")) import scheduler_locale # noqa: E402 SCRATCH = HERE / "_scratch_scheduler_locale" LOG_DIR = SCRATCH / "logs" def _setup() -> Path: if SCRATCH.exists(): shutil.rmtree(SCRATCH) SCRATCH.mkdir(parents=True) LOG_DIR.mkdir(parents=True) shutil.copy2(HERE.parent / "ufficio" / "run_job.py", SCRATCH / "run_job.py") (SCRATCH / "fake_job.bat").write_text( "@echo off\necho demo job eseguito\nexit /b 0\n", encoding="utf-8" ) (SCRATCH / "fake_job_fail.bat").write_text( "@echo off\necho demo job fallito\nexit /b 1\n", encoding="utf-8" ) schedule = { "demo_job_ok": {"cron": "* * * * *", "command": "fake_job.bat", "timeout_seconds": 30}, "demo_job_fail": {"cron": "* * * * *", "command": "fake_job_fail.bat", "timeout_seconds": 30}, "demo_job_off": {"cron": "0 0 1 1 *", "command": "fake_job.bat", "timeout_seconds": 30}, } schedule_path = SCRATCH / "schedule.json" schedule_path.write_text(json.dumps(schedule, indent=2), encoding="utf-8") return schedule_path def _wait_for_esito(log_glob: str, timeout: float = 20.0) -> str | None: deadline = time.time() + timeout while time.time() < deadline: candidates = sorted(LOG_DIR.glob(log_glob)) if candidates: content = candidates[-1].read_text(encoding="utf-8", errors="replace") if "=== ESITO:" in content: return content time.sleep(0.5) return None def main(): schedule_path = _setup() # Isolamento: per la durata del test, dispatch_job()/run_loop() devono # risolvere "command" e scrivere i log dentro lo scratch, non in # ufficio/ reale. scheduler_locale.UFFICIO_ROOT = SCRATCH scheduler_locale.LOG_DIR = LOG_DIR try: # --- is_due() sul nuovo schema {job_name: {cron, ...}} --- schedule = scheduler_locale.load_schedule(schedule_path) now = datetime.now() assert scheduler_locale.is_due(schedule["demo_job_ok"]["cron"], now) is True assert scheduler_locale.is_due(schedule["demo_job_off"]["cron"], now) is False print("is_due() su schema esteso {cron, command, timeout_seconds} OK") # --- dispatch_job() diretto: verifica formato log (successo e fallimento) --- scheduler_locale.dispatch_job("demo_job_ok", schedule["demo_job_ok"]) scheduler_locale.dispatch_job("demo_job_fail", schedule["demo_job_fail"]) print("Job fittizi lanciati (fire-and-forget), attendo gli ESITO...") content_ok = _wait_for_esito("demo_job_ok_*.log") assert content_ok is not None, "demo_job_ok: nessun ESITO entro il timeout" assert "=== ESITO: completed exit_code=0" in content_ok, f"demo_job_ok: esito inatteso:\n{content_ok}" print("demo_job_ok -> completed exit_code=0 OK") content_fail = _wait_for_esito("demo_job_fail_*.log") assert content_fail is not None, "demo_job_fail: nessun ESITO entro il timeout" assert "=== ESITO: failed exit_code=1" in content_fail, f"demo_job_fail: esito inatteso:\n{content_fail}" print("demo_job_fail -> failed exit_code=1 OK") # --- run_loop(--once): dispatcha solo i job dovuti, non quello off-schedule --- before = {p.name for p in LOG_DIR.glob("*.log")} scheduler_locale.run_loop(schedule_path, once=True) time.sleep(3) # i job spawnati da run_loop sono anch'essi fire-and-forget after = {p.name for p in LOG_DIR.glob("*.log")} new_logs = after - before assert any(n.startswith("demo_job_ok_") for n in new_logs), f"run_loop --once non ha dispatchato demo_job_ok: {new_logs}" assert not any(n.startswith("demo_job_off_") for n in new_logs), f"run_loop --once ha dispatchato demo_job_off per errore: {new_logs}" print("run_loop(--once) dispatcha solo i job dovuti OK") print("\nTutti i controlli end-to-end (scheduler_locale + run_job) superati.") finally: shutil.rmtree(SCRATCH, ignore_errors=True) if __name__ == "__main__": main()