debito 7: la cadenza del libro e' ORARIA — docstring riscritto, tre fonti tenute d'accordo da un test
Il docstring di book_execute.py prescriveva «ogni ~230 minuti» mentre il cron gira ogni ora (47 * * * *). Era il docstring a sbagliare: sulla riga 4h BTC raddoppia gli scatti del disaster-SL rotolante (r0823_sl_anchor.py), e chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore. - scripts/live/book_execute.py: CADENZA: ORARIA, la riga di crontab, la ragione (giro idempotente, latenza SKH01 <=1h, ri-ancoraggio orario) e il divieto esplicito con la data. - tests/test_book_cadenza.py (10): deriva e confronta docstring, intestazione di cron_book.sh e crontab installata (crontab -l; SALTATO se illeggibile, non verde); parser a 5 campi solo per cadenze regolari; 60 < 230 < 240; minuto != :00. - docs: CLAUDE.md §5.7 chiuso, §13 conteggio 842; memoria 40; diario 02/09b. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RziUCB336YPUUyDJ29x4Ke
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""La cadenza del libro live e' ORARIA, e le tre dichiarazioni devono restare d'accordo.
|
||||
|
||||
Debito §5.7 (chiuso il 2026-09-02): il docstring di `book_execute.py` prescriveva «ogni ~230
|
||||
minuti» mentre il cron gira ogni ora. Non era cosmetico: `r0823_sl_anchor.py` misura che sulla
|
||||
riga 4h BTC RADDOPPIA gli scatti del disaster-SL rotolante (2 contro 1; a 24h ETH arriva a 5).
|
||||
Chi avesse "corretto" il cron verso il docstring avrebbe spostato il libro sulla riga peggiore.
|
||||
|
||||
P1: il bersaglio si DERIVA da cio' che gira. Qui le fonti sono tre — il docstring (cio' che un
|
||||
umano legge), `cron_book.sh` (cio' che dichiara di essere installato) e la crontab (cio' che
|
||||
gira davvero) — e il test le confronta fra loro invece di ridichiarare «60 minuti».
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BOOK_EXECUTE = ROOT / "scripts" / "live" / "book_execute.py"
|
||||
CRON_BOOK = ROOT / "scripts" / "cron_book.sh"
|
||||
GRIGLIA_SKH_MIN = 230 # data1 di SKH01 (src/strategies/skyhook.py): la riga da NON usare
|
||||
RIGA_PEGGIORE_MIN = 240 # 4h: dove BTC raddoppia gli scatti (r0823_sl_anchor.py)
|
||||
|
||||
|
||||
def cadenza_minuti(expr: str) -> int:
|
||||
"""Minuti fra due esecuzioni di un'espressione crontab a 5 campi, per i soli casi regolari."""
|
||||
campi = expr.split()
|
||||
assert len(campi) == 5, expr
|
||||
m, h, dom, mon, dow = campi
|
||||
assert dom == mon == dow == "*", f"cadenza non regolare: {expr}"
|
||||
if h == "*" and m.isdigit():
|
||||
return 60
|
||||
if h == "*" and m.startswith("*/"):
|
||||
return int(m[2:])
|
||||
if h.startswith("*/") and m.isdigit():
|
||||
return 60 * int(h[2:])
|
||||
raise ValueError(f"espressione non riconosciuta: {expr}")
|
||||
|
||||
|
||||
def _docstring() -> str:
|
||||
return ast.get_docstring(ast.parse(BOOK_EXECUTE.read_text())) or ""
|
||||
|
||||
|
||||
def _cron_dichiarato() -> str:
|
||||
"""L'espressione crontab che `cron_book.sh` dichiara nel suo intestazione, fra backtick."""
|
||||
m = re.search(r"`(\d+ \* \* \* \*)`", CRON_BOOK.read_text())
|
||||
assert m, "cron_book.sh non dichiara piu' la sua riga di crontab"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def _cron_installato() -> str | None:
|
||||
if not shutil.which("crontab"):
|
||||
return None
|
||||
try:
|
||||
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||
except Exception:
|
||||
return None
|
||||
if out.returncode != 0:
|
||||
return None
|
||||
for riga in out.stdout.splitlines():
|
||||
if "cron_book.sh" in riga and not riga.lstrip().startswith("#"):
|
||||
return " ".join(riga.split()[:5])
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- regressione sul docstring
|
||||
|
||||
def test_il_docstring_non_prescrive_piu_i_230_minuti():
|
||||
d = _docstring()
|
||||
assert "ogni ~230" not in d and "ogni 230" not in d
|
||||
assert re.search(r"CADENZA:\s*ORARIA", d), "il docstring deve dichiarare la cadenza ORARIA"
|
||||
|
||||
|
||||
def test_il_docstring_dichiara_la_stessa_riga_del_cron():
|
||||
# la riga di crontab citata nel docstring e' quella di cron_book.sh, non un'altra
|
||||
d = _docstring()
|
||||
assert _cron_dichiarato() in d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- cron_book.sh
|
||||
|
||||
def test_cron_book_e_orario_e_sta_lontano_dalla_riga_peggiore():
|
||||
expr = _cron_dichiarato()
|
||||
c = cadenza_minuti(expr)
|
||||
assert c == 60
|
||||
assert c < GRIGLIA_SKH_MIN < RIGA_PEGGIORE_MIN
|
||||
assert "ORARIA" in CRON_BOOK.read_text()
|
||||
|
||||
|
||||
def test_cron_book_evita_il_minuto_tondo():
|
||||
# vincolo 1 del commento di cron_book.sh: fuori dai ~26s del :00 (rate-limit per-IP)
|
||||
minuto = int(_cron_dichiarato().split()[0])
|
||||
assert minuto != 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expr,atteso", [("47 * * * *", 60), ("*/30 * * * *", 30),
|
||||
("7 */4 * * *", 240), ("0 */24 * * *", 1440)])
|
||||
def test_il_parser_della_cadenza(expr, atteso):
|
||||
assert cadenza_minuti(expr) == atteso
|
||||
|
||||
|
||||
def test_il_parser_rifiuta_cio_che_non_sa_leggere():
|
||||
with pytest.raises((ValueError, AssertionError)):
|
||||
cadenza_minuti("47 9 * * 2")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- la crontab che GIRA
|
||||
|
||||
def test_la_crontab_installata_e_quella_dichiarata():
|
||||
"""P16: il cron gira dalla macchina, non dal repo. Dove la crontab e' leggibile (la VPS),
|
||||
deve coincidere con cio' che cron_book.sh dichiara; altrove il test si dichiara saltato,
|
||||
non verde (P5: 'non vedo' non e' 'va tutto bene')."""
|
||||
vivo = _cron_installato()
|
||||
if vivo is None:
|
||||
pytest.skip("crontab non leggibile o senza cron_book.sh: non verificabile qui")
|
||||
assert vivo == _cron_dichiarato()
|
||||
assert cadenza_minuti(vivo) == 60
|
||||
Reference in New Issue
Block a user