"""Un flag ignoto non e' l'azione di default (2026-09-02). Costo misurato durante una revisione dello stesso giorno: `trades_db.py --help` non stampava l'uso, cadeva in `sync()` e riscriveva `meta.ultimo_sync`. Su `journal.py` lo stesso errore avrebbe scritto una pagina e una riga di DB; su `analista.py` avrebbe speso una chiamata al modello e mandato un Telegram. Il test guarda anche l'altra meta' del contratto (P15): i flag che il CRON usa devono restare accettati — una guardia che rompe il cron e' peggio del difetto che ripara. """ from __future__ import annotations import ast import re import subprocess import sys from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) from src.live.cli import UsoSbagliato, valida # noqa: E402 # Tutti gli script di scripts/live/ che NON usano argparse: la guardia deve coprirli tutti, e # l'elenco si DERIVA dalla cartella (P1) — un file nuovo senza guardia fa fallire il test, non # passa inosservato perche' nessuno ha aggiornato una lista. LIVE = Path(__file__).resolve().parents[1] / "scripts" / "live" def _senza_argparse() -> list[str]: fuori = [] for f in sorted(LIVE.glob("*.py")): s = f.read_text() if f.name == "__init__.py" or "argparse" in s or "if __name__" not in s: continue fuori.append(f.name) return fuori SCRIPT_GUARDATI = tuple(_senza_argparse()) assert SCRIPT_GUARDATI, "nessuno script da guardare: il modulo non sta controllando niente" SCRIVONO = ("trades_db.py", "journal.py", "analista.py") # i tre che hanno pagato il difetto # ---------------------------------------------------------------- la funzione def _v(argv, **kw): return valida("x.py", "uso: x.py", argv=argv, esci=False, **kw) def test_un_flag_noto_passa_e_torna_gli_argomenti(): assert _v(["--report"], flag=("--report",)) == ["--report"] def test_un_flag_ignoto_e_un_errore_non_il_default(): with pytest.raises(UsoSbagliato) as e: _v(["--help-me"], flag=("--report",)) assert "--help-me" in str(e.value) and "--report" in str(e.value) # P4: cosa, e cosa si puo' def test_il_valore_di_un_flag_non_e_scambiato_per_un_flag(): assert _v(["--giorno", "2026-08-21"], con_valore=("--giorno",)) == ["--giorno", "2026-08-21"] with pytest.raises(UsoSbagliato): _v(["--giorno"], con_valore=("--giorno",)) # manca il valore def test_un_valore_che_sembra_un_flag_resta_un_valore(): assert _v(["--nota", "--quiet"], con_valore=("--nota",), flag=("--quiet",)) def test_un_posizionale_e_ignoto(): with pytest.raises(UsoSbagliato): _v(["report"], flag=("--report",)) def test_senza_argomenti_non_solleva(): assert _v([], flag=("--report",)) == [] def test_help_non_e_un_errore(): assert _v(["--help"], flag=("--report",)) == ["--help"] # ---------------------------------------------------------------- gli script veri def _uso(nome: str) -> str: """La costante USO dello script, letta dal sorgente senza importarlo.""" src = (ROOT / "scripts" / "live" / nome).read_text() for n in ast.parse(src).body: if isinstance(n, ast.Assign) and getattr(n.targets[0], "id", "") == "USO": return n.value.value raise AssertionError(f"{nome} non dichiara USO") @pytest.mark.parametrize("nome", SCRIPT_GUARDATI) def test_lo_script_chiama_valida_prima_di_ogni_effetto(nome): """`valida` dev'essere la PRIMA istruzione eseguibile del blocco __main__: uscire dopo aver aperto il DB o sincronizzato sarebbe uscire dopo l'effetto.""" src = (ROOT / "scripts" / "live" / nome).read_text() main = [n for n in ast.parse(src).body if isinstance(n, ast.If) and "__main__" in ast.dump(n.test)] assert main, f"{nome}: nessun blocco __main__" corpo = [n for n in main[0].body if not isinstance(n, ast.ImportFrom | ast.Import)] chiamate = [n for n in ast.walk(corpo[0]) if isinstance(n, ast.Call)] assert any(getattr(c.func, "id", "") == "valida" for c in chiamate), \ f"{nome}: la prima istruzione di __main__ non e' valida()" @pytest.mark.parametrize("nome", SCRIPT_GUARDATI) def test_l_uso_elenca_esattamente_i_flag_dichiarati(nome): """P4 e P1: il testo d'aiuto non e' prosa libera — deve nominare i flag che lo script accetta.""" src = (ROOT / "scripts" / "live" / nome).read_text() dichiarati = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1])) uso = _uso(nome) mancanti = {f for f in dichiarati if f not in uso} assert not mancanti, f"{nome}: flag accettati ma non documentati in USO: {mancanti}" # I flag che il cron usa DAVVERO, letti dagli script di cron: se la guardia ne rifiutasse uno, # il libro di bordo si fermerebbe la notte stessa (P16: il cron gira dal working tree). def test_i_flag_del_cron_sono_tutti_accettati(): usati: dict[str, set[str]] = {} for sh in (ROOT / "scripts").glob("cron_*.sh"): for riga in sh.read_text().splitlines(): for nome in SCRIPT_GUARDATI: if f"scripts/live/{nome}" in riga and not riga.lstrip().startswith("#"): usati.setdefault(nome, set()).update(re.findall(r"(--[a-z-]+)", riga)) assert usati, "nessun cron chiama gli script guardati: il test non sta controllando niente" for nome, flag in usati.items(): src = (ROOT / "scripts" / "live" / nome).read_text() noti = set(re.findall(r'["\'](--[a-z-]+)["\']', src.split("if __name__")[1])) assert flag <= noti, f"{nome}: il cron usa {flag - noti}, che la guardia rifiuterebbe" # controllo positivo (M15): il confronto sa dire di no. Se il cron usasse un flag non # dichiarato, questo test lo direbbe — qui lo si verifica su un caso finto. assert not {"--inventato"} <= {"--quiet"} @pytest.mark.parametrize("nome", SCRIVONO) def test_help_esce_zero_e_non_scrive(nome): """Controllo end-to-end (M15): il caso che ha causato il difetto, sui tre script veri.""" prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--help"], capture_output=True, text=True, cwd=ROOT, timeout=120) assert r.returncode == 0, r.stderr[-500:] assert "uso:" in r.stdout assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima @pytest.mark.parametrize("nome", SCRIVONO) def test_un_flag_ignoto_esce_due_e_non_scrive(nome): prima = (ROOT / "data" / "live" / "trades.db").stat().st_mtime r = subprocess.run([sys.executable, str(ROOT / "scripts" / "live" / nome), "--nonesiste"], capture_output=True, text=True, cwd=ROOT, timeout=120) assert r.returncode == 2, r.stdout[-300:] assert "non previsto" in r.stderr assert (ROOT / "data" / "live" / "trades.db").stat().st_mtime == prima def test_ogni_script_senza_argparse_ha_la_guardia(): """L'elenco e' derivato dalla cartella: uno script nuovo che legge sys.argv a mano e non chiama `valida` fa fallire questo test invece di ripetere il difetto in silenzio.""" senza = [n for n in SCRIPT_GUARDATI if "valida(" not in (LIVE / n).read_text().split("if __name__")[1]] assert not senza, f"script senza guardia sui flag: {senza}" assert len(SCRIPT_GUARDATI) >= 17, f"troppo pochi script guardati: {SCRIPT_GUARDATI}"