"""Scheduler: alleen afstempelen als een taak echt gedraaid heeft. Dat onderscheid is niet cosmetisch. Een taak die zijn eigen tijdvenster bewaakt (zoals de geplande backup om 03:00) wordt bij elke tick even aangeroepen. Zou de scheduler ook een overgeslagen taak afstempelen, dan verschuift het volgende moment naar precies dat verkeerde tijdstip — en draait de backup nooit. """ from datetime import datetime import pytest @pytest.fixture def sch(env): from core import scheduler scheduler._TAKEN.clear() return scheduler def test_overgeslagen_taak_wordt_niet_afgestempeld(sch, env): aanroepen = {"n": 0} def _slaat_over(): aanroepen["n"] += 1 return False sch.register("nooit", _slaat_over, interval_uren=24) assert sch.run_due() == [] assert sch.run_due() == [] # Elke tick opnieuw geprobeerd, want er is niets afgestempeld. assert aanroepen["n"] == 2 assert "nooit" not in env["core"].load().get("SCHEDULER_LAST", {}) def test_uitgevoerde_taak_wordt_wel_afgestempeld(sch, env): aanroepen = {"n": 0} def _doet_iets(): aanroepen["n"] += 1 return True sch.register("wel", _doet_iets, interval_uren=24) assert sch.run_due() == ["wel"] assert sch.run_due() == [] # interval nog niet verstreken assert aanroepen["n"] == 1 assert "wel" in env["core"].load()["SCHEDULER_LAST"] def test_taak_draait_opnieuw_na_het_interval(sch, env): import time sch.register("kort", lambda: True, interval_uren=1) assert sch.run_due() == ["kort"] assert sch.run_due() == [] # Tijdstempel terugzetten in plaats van een uur wachten. env["core"].patch({"SCHEDULER_LAST": {"kort": time.time() - 3700}}) assert sch.run_due() == ["kort"] def test_falende_taak_wordt_afgestempeld(sch, env): """Anders probeert een structureel kapotte taak het elke tick opnieuw.""" aanroepen = {"n": 0} def _ontploft(): aanroepen["n"] += 1 raise RuntimeError("stuk") sch.register("stuk", _ontploft, interval_uren=24) assert sch.run_due() == [] assert sch.run_due() == [] assert aanroepen["n"] == 1 assert "stuk" in env["core"].load()["SCHEDULER_LAST"] def test_enabled_key_schakelt_taak_uit(sch, env): env["core"].patch({"MIJN_VLAG": False}) sch.register("uit", lambda: True, interval_uren=0.001, enabled_key="MIJN_VLAG") assert sch.run_due() == [] env["core"].patch({"MIJN_VLAG": True}) assert sch.run_due() == ["uit"] def test_een_kapotte_taak_blokkeert_de_rest_niet(sch, env): gedaan = [] sch.register("a", lambda: (_ for _ in ()).throw(RuntimeError("stuk"))) sch.register("b", lambda: (gedaan.append("b"), True)[1]) sch.run_due() assert gedaan == ["b"] # ── Tijdvenster ────────────────────────────────────────────────────────────── @pytest.mark.parametrize("nu,uur,verwacht", [ (datetime(2026, 7, 26, 3, 0), 3, True), (datetime(2026, 7, 26, 3, 29), 3, True), (datetime(2026, 7, 26, 3, 30), 3, False), (datetime(2026, 7, 26, 2, 59), 3, False), (datetime(2026, 7, 26, 14, 0), 3, False), ]) def test_binnen_venster(sch, nu, uur, verwacht): assert sch.binnen_venster(uur, 30, nu=nu) is verwacht # ── De backup-taak zelf ────────────────────────────────────────────────────── def test_geplande_backup_slaat_over_buiten_het_venster(env, monkeypatch): import app as app_module from core import scheduler env["core"].patch({"BACKUP_SCHEDULE": "daily", "BACKUP_SCHEDULE_HOUR": 3}) monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: False) assert app_module._geplande_backups() is False def test_geplande_backup_slaat_over_als_planning_uit_staat(env): import app as app_module env["core"].patch({"BACKUP_SCHEDULE": "off"}) assert app_module._geplande_backups() is False def test_geplande_backup_draait_binnen_het_venster(env, monkeypatch): import app as app_module from core import scheduler, backups env["core"].patch({"BACKUP_SCHEDULE": "daily", "BACKUP_SCHEDULE_HOUR": 3, "BACKUP_DIR": str(env["tmp"] / "backups")}) d = env["lib"] / "web" d.mkdir(parents=True) (d / "docker-compose.yml").write_text("services:\n a:\n image: nginx\n") monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: True) assert app_module._geplande_backups() is True assert len(backups.list_for("web")) == 1 assert backups.list_for("web")[0]["reason"] == "gepland-daily" def test_wekelijkse_backup_alleen_op_de_ingestelde_dag(env, monkeypatch): import app as app_module from core import scheduler env["core"].patch({"BACKUP_SCHEDULE": "weekly", "BACKUP_SCHEDULE_DAY": 0}) monkeypatch.setattr(scheduler, "binnen_venster", lambda *a, **k: True) class _Dinsdag(datetime): @classmethod def now(cls, tz=None): return datetime(2026, 7, 28) # dinsdag → weekday() == 1 monkeypatch.setattr(app_module, "datetime", _Dinsdag) assert app_module._geplande_backups() is False