Semplifica: CLI a due comandi, moduli piatti, solo modelli usati; test unitari su segnale sintetico
- rimossi scripts/, stadi.py, docs/USO.md, modelli non usati, partenze casuali, output ridondanti - uscita unica (report.md con grafici per dominio, risultati.json, proposta.png) - test: unitari su segnale sintetico, integrazione su dati/gen_*.csv (saltati se mancano) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8c8aeb60e
commit
d92bcd198b
22 files changed
+678
-683
No files matched your search
@@ -0,0 +1,43 @@
|
||||
"""Fixture comuni: configurazione, segnale sintetico (verità nota) e CSV generati da gen_data (se presenti)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from fitseg import config
|
||||
|
||||
RADICE = Path(__file__).parent.parent
|
||||
SEEDS = [1, 2, 3]
|
||||
|
||||
# verità del segnale sintetico
|
||||
TAU, T_AMB = 16.0, 23.0
|
||||
GRADINI = [(15.0, 80.0), (18.0, 70.0), (22.0, 60.0)] # (istante [s], ampiezza [°C])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def cfg():
|
||||
return config.leggi(RADICE / "config.toml")
|
||||
|
||||
|
||||
def segnale(rumore=0.1, seed=0, gradini=GRADINI, durata=120.0, dt=0.15):
|
||||
"""T(t) = ambiente + Σ ampiezza·exp(−(t−t_j)/τ) per t ≥ t_j, più rumore gaussiano: nessun disturbo a salto."""
|
||||
rng = np.random.default_rng(seed)
|
||||
t = np.arange(dt, durata, dt)
|
||||
x = sum(a * np.exp(-(t - tj) / TAU) * (t >= tj) for tj, a in gradini) if gradini else np.zeros_like(t)
|
||||
T = T_AMB + x + rumore * rng.standard_normal(len(t))
|
||||
return t, {"T_C": T, "dT_C": T - T_AMB}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sintetico():
|
||||
return segnale()
|
||||
|
||||
|
||||
@pytest.fixture(params=SEEDS)
|
||||
def csv_gen(request):
|
||||
"""CSV prodotto da gen_data per il seed; il test è saltato se il file non c'è (dati/ non è in git)."""
|
||||
percorso = RADICE / f"dati/gen_{request.param}.csv"
|
||||
if not percorso.exists():
|
||||
pytest.skip(f"{percorso} assente: rigenera con gen_data (vedi README)")
|
||||
return percorso
|
||||
@@ -0,0 +1,46 @@
|
||||
"""Lettura di config.toml e del CSV: errori attesi."""
|
||||
|
||||
import pytest
|
||||
from conftest import RADICE
|
||||
|
||||
from fitseg import config, dati
|
||||
|
||||
|
||||
def test_config_reale_valida():
|
||||
assert config.leggi(RADICE / "config.toml")["dati"]["serie"] == ["T_C", "dT_C"]
|
||||
|
||||
|
||||
def test_chiave_mancante(tmp_path):
|
||||
(tmp_path / "c.toml").write_text("[dati]\ncolonna_tempo='t'\n")
|
||||
with pytest.raises(config.ErroreConfig, match="chiavi mancanti"):
|
||||
config.leggi(tmp_path / "c.toml")
|
||||
|
||||
|
||||
def test_modelli_per_tipo_incompleto(tmp_path):
|
||||
testo = (RADICE / "config.toml").read_text().replace('raffreddamento = ["esp_decad"]', "raffreddamento = []")
|
||||
(tmp_path / "c.toml").write_text(testo)
|
||||
with pytest.raises(config.ErroreConfig, match="raffreddamento"):
|
||||
config.leggi(tmp_path / "c.toml")
|
||||
|
||||
|
||||
def test_config_inesistente():
|
||||
with pytest.raises(config.ErroreConfig, match="impossibile leggere"):
|
||||
config.leggi("/non/esiste.toml")
|
||||
|
||||
|
||||
def test_csv_valido(cfg, tmp_path):
|
||||
(tmp_path / "d.csv").write_text("t_ms,T_C,dT_C\n100,20.0,0.1\n250,20.1,0.2\n")
|
||||
t, serie = dati.leggi_csv(tmp_path / "d.csv", cfg)
|
||||
assert t.tolist() == [0.1, 0.25] and serie["T_C"].tolist() == [20.0, 20.1]
|
||||
|
||||
|
||||
def test_csv_colonna_mancante(cfg, tmp_path):
|
||||
(tmp_path / "d.csv").write_text("t_ms,T_C\n100,20.0\n")
|
||||
with pytest.raises(dati.ErroreDati, match="dT_C"):
|
||||
dati.leggi_csv(tmp_path / "d.csv", cfg)
|
||||
|
||||
|
||||
def test_csv_tempo_non_crescente(cfg, tmp_path):
|
||||
(tmp_path / "d.csv").write_text("t_ms,T_C,dT_C\n200,20,0\n100,20,0\n")
|
||||
with pytest.raises(dati.ErroreDati, match="crescente"):
|
||||
dati.leggi_csv(tmp_path / "d.csv", cfg)
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Riconoscimento dei domini (riposo / riscaldamento / raffreddamento) su segnali sintetici."""
|
||||
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
from conftest import GRADINI, segnale
|
||||
|
||||
from fitseg import domini
|
||||
|
||||
|
||||
def tipi(t, serie, cfg):
|
||||
return [d["tipo"] for d in domini.proponi(t, serie, cfg)]
|
||||
|
||||
|
||||
def test_tre_gradini(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
assert tipi(t, serie, cfg) == ["riposo", "riscaldamento", "riscaldamento", "raffreddamento"]
|
||||
|
||||
|
||||
def test_confini_vicini_agli_istanti_dei_gradini(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
for d, (tj, _) in zip(ds[1:], GRADINI, strict=True):
|
||||
assert abs(t[d["i0"]] - tj) < 0.5
|
||||
|
||||
|
||||
def test_domini_contigui_e_completi(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
assert ds[0]["i0"] == 0 and ds[-1]["i1"] == len(t)
|
||||
assert all(a["i1"] == b["i0"] for a, b in itertools.pairwise(ds))
|
||||
|
||||
|
||||
def test_senza_gradini_un_solo_riposo(cfg):
|
||||
t, serie = segnale(gradini=[])
|
||||
assert tipi(t, serie, cfg) == ["riposo"]
|
||||
|
||||
|
||||
def test_un_solo_gradino(cfg):
|
||||
t, serie = segnale(gradini=[(20.0, 80.0)])
|
||||
assert tipi(t, serie, cfg) == ["riposo", "raffreddamento"]
|
||||
|
||||
|
||||
def test_gradino_sotto_soglia_ignorato(cfg):
|
||||
t, serie = segnale(gradini=[(20.0, 10.0)]) # 10 °C < soglia_gradino_c
|
||||
assert tipi(t, serie, cfg) == ["riposo"]
|
||||
|
||||
|
||||
def test_domini_toml_andata_e_ritorno(sintetico, cfg, tmp_path):
|
||||
t, serie = sintetico
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
domini.scrivi_toml(tmp_path / "d.toml", ds, t)
|
||||
assert domini.leggi_toml(tmp_path / "d.toml", t) == ds
|
||||
|
||||
|
||||
def test_domini_toml_non_contigui_rifiutati(sintetico, tmp_path):
|
||||
t, _ = sintetico
|
||||
(tmp_path / "d.toml").write_text('[[dominio]]\ntipo="riposo"\nt0_s=0\nt1_s=10\n'
|
||||
'[[dominio]]\ntipo="raffreddamento"\nt0_s=20\nt1_s=30\n')
|
||||
with pytest.raises(ValueError, match="contigui"):
|
||||
domini.leggi_toml(tmp_path / "d.toml", t)
|
||||
|
||||
|
||||
def test_domini_toml_tipo_non_valido(sintetico, tmp_path):
|
||||
t, _ = sintetico
|
||||
(tmp_path / "d.toml").write_text('[[dominio]]\ntipo="boh"\nt0_s=0\nt1_s=10\n')
|
||||
with pytest.raises(ValueError, match="non valido"):
|
||||
domini.leggi_toml(tmp_path / "d.toml", t)
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Fit, regola di scelta e analisi dei domini su segnali sintetici (verità nota: τ = 16 s, ambiente 23 °C)."""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from conftest import GRADINI, TAU, segnale
|
||||
|
||||
from fitseg import domini
|
||||
from fitseg.analisi import analizza_domini
|
||||
from fitseg.fit import adatta
|
||||
from fitseg.modelli import MODELLI, valuta
|
||||
|
||||
|
||||
def test_modelli_valutazione():
|
||||
t = np.array([0.0, 1.0])
|
||||
assert valuta("costante", t, [5.0]).tolist() == [5.0, 5.0]
|
||||
assert valuta("lineare", t, [1.0, 2.0]).tolist() == [1.0, 3.0]
|
||||
assert valuta("esp_decad", t, [1.0, 2.0, 1.0])[0] == pytest.approx(3.0)
|
||||
assert set(MODELLI) == {"costante", "lineare", "esp_decad"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(5))
|
||||
def test_esp_decad_recupera_parametri(cfg, seed):
|
||||
rng = np.random.default_rng(seed)
|
||||
t = np.arange(0, 100, 0.15)
|
||||
y = 23 + 180 * np.exp(-t / 16) + 0.1 * rng.standard_normal(len(t))
|
||||
fit = adatta(t, y, "esp_decad", 0.1, cfg)
|
||||
p = fit["parametri"]
|
||||
assert p["tau"] == pytest.approx(16, rel=0.02)
|
||||
assert p["c"] == pytest.approx(23, abs=0.2)
|
||||
assert p["A"] == pytest.approx(180, rel=0.02)
|
||||
assert fit["rmse"] == pytest.approx(0.1, rel=0.2)
|
||||
assert not fit["non_identificabile"] and not fit["parametri_a_limite"]
|
||||
|
||||
|
||||
def test_parametri_fissi_restano_fissi(cfg):
|
||||
rng = np.random.default_rng(0)
|
||||
t = np.arange(0, 4, 0.15)
|
||||
y = 23 + 80 * np.exp(-t / 16) + 0.1 * rng.standard_normal(len(t))
|
||||
fit = adatta(t, y, "esp_decad", 0.1, cfg, fissi={"c": 23.0, "tau": 16.0})
|
||||
assert fit["fissi"] == ["c", "tau"]
|
||||
assert fit["parametri"]["c"] == 23.0 and fit["parametri"]["tau"] == 16.0
|
||||
assert fit["n_parametri"] == 1
|
||||
assert fit["parametri"]["A"] == pytest.approx(80, rel=0.03)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", range(5))
|
||||
def test_tau_libero_non_identificabile_su_tratto_corto_e_rumoroso(cfg, seed):
|
||||
# 3 s di dati con disturbi dell'ordine degli scarti casuali del sensore (≈ 2 °C): τ non è stimabile da solo
|
||||
rng = np.random.default_rng(seed)
|
||||
t = np.arange(0, 3, 0.15)
|
||||
y = 23 + 80 * np.exp(-t / 16) + 2.0 * rng.standard_normal(len(t))
|
||||
assert adatta(t, y, "esp_decad", 2.0, cfg)["non_identificabile"]
|
||||
|
||||
|
||||
def test_analisi_completa_sintetico(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
assert [r["tipo"] for r in ris] == ["riposo", "riscaldamento", "riscaldamento", "raffreddamento"]
|
||||
for nome in serie:
|
||||
assert ris[0]["serie"][nome]["scelta"] in ("costante", "lineare")
|
||||
raff = next(x for x in ris[3]["serie"][nome]["fit"] if x["modello"] == "esp_decad")["parametri"]
|
||||
assert raff["tau"] == pytest.approx(TAU, rel=0.03)
|
||||
for r in ris[1:3]: # riscaldamento: c e τ ereditati dal raffreddamento
|
||||
fit = next(x for x in r["serie"][nome]["fit"] if x["modello"] == "esp_decad")
|
||||
assert r["serie"][nome]["scelta"] == "esp_decad" and fit["fissi"] == ["c", "tau"]
|
||||
assert fit["parametri"]["tau"] == raff["tau"] and fit["parametri"]["c"] == raff["c"]
|
||||
# ampiezza del secondo tratto = somma dei decadimenti dei gradini precedenti + il nuovo, in t'=0
|
||||
t1 = ris[1]["t0_s"]
|
||||
attesa = sum(a * np.exp(-(t1 - tj) / TAU) for tj, a in GRADINI[:1])
|
||||
fit1 = next(x for x in ris[1]["serie"]["dT_C"]["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit1["parametri"]["A"] == pytest.approx(attesa, rel=0.05)
|
||||
|
||||
|
||||
def test_senza_raffreddamento_riscaldamento_resta_libero(cfg):
|
||||
t, serie = segnale(gradini=[(20.0, 80.0)], durata=40.0)
|
||||
ds = [{"tipo": "riposo", "i0": 0, "i1": 100}, {"tipo": "riscaldamento", "i0": 100, "i1": len(t)}]
|
||||
ris = analizza_domini(t, serie, ds, cfg) # nessun dominio di raffreddamento: niente parametri da ereditare
|
||||
for x in ris[1]["serie"]["T_C"]["fit"]:
|
||||
assert x["fissi"] == []
|
||||
|
||||
|
||||
def test_dominio_troppo_corto_indeterminato(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = [{"tipo": "riposo", "i0": 0, "i1": 5}]
|
||||
vs = analizza_domini(t, serie, ds, cfg)[0]["serie"]["T_C"]
|
||||
assert vs["scelta"] is None and "indeterminato" in vs["motivo"]
|
||||
|
||||
|
||||
def test_riproducibile(sintetico, cfg):
|
||||
t, serie = sintetico
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
a = analizza_domini(t, serie, ds, cfg)
|
||||
b = analizza_domini(t, serie, ds, cfg)
|
||||
assert a == b
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Integrazione sui CSV di gen_data (dati/gen_{1,2,3}.csv, ignorati da git: i test sono saltati se mancano) e sulla CLI."""
|
||||
|
||||
import pytest
|
||||
from conftest import RADICE
|
||||
|
||||
from fitseg import cli, dati, domini
|
||||
from fitseg.analisi import analizza_domini
|
||||
|
||||
|
||||
def analizza(csv, cfg):
|
||||
t, serie = dati.leggi_csv(csv, cfg)
|
||||
ds = domini.proponi(t, serie, cfg)
|
||||
return ds, analizza_domini(t, serie, ds, cfg)
|
||||
|
||||
|
||||
def test_domini_su_csv_gen(csv_gen, cfg):
|
||||
ds, _ = analizza(csv_gen, cfg)
|
||||
assert [d["tipo"] for d in ds] == ["riposo", "riscaldamento", "riscaldamento", "raffreddamento"]
|
||||
|
||||
|
||||
def test_raffreddamento_tau_nominale(csv_gen, cfg):
|
||||
_, ris = analizza(csv_gen, cfg) # τ nominale 1/k = 15.9 s, k perturbato del 2 % per seed
|
||||
for vs in ris[-1]["serie"].values():
|
||||
assert vs["scelta"] == "esp_decad"
|
||||
fit = next(x for x in vs["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["parametri"]["tau"] == pytest.approx(15.9, rel=0.1)
|
||||
|
||||
|
||||
def test_riscaldamento_eredita_costanti(csv_gen, cfg):
|
||||
_, ris = analizza(csv_gen, cfg)
|
||||
for nome in ris[0]["serie"]:
|
||||
raff = next(x for x in ris[-1]["serie"][nome]["fit"] if x["modello"] == "esp_decad")["parametri"]
|
||||
for r in ris[1:3]:
|
||||
fit = next(x for x in r["serie"][nome]["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["fissi"] == ["c", "tau"] and fit["parametri"]["tau"] == raff["tau"] and fit["parametri"]["A"] > 0
|
||||
|
||||
|
||||
def test_cli_domini_e_analizza(tmp_path, capsys):
|
||||
csv = RADICE / "dati/gen_1.csv"
|
||||
if not csv.exists():
|
||||
pytest.skip(f"{csv} assente")
|
||||
lavoro = tmp_path / "lavoro"
|
||||
base = ["--config", str(RADICE / "config.toml"), "--lavoro", str(lavoro)]
|
||||
cli.main([*base, "domini", str(csv)])
|
||||
assert (lavoro / "domini.toml").exists() and (lavoro / "proposta.png").exists()
|
||||
assert not (lavoro / "out").exists() # `domini` non fa nessun fit
|
||||
cli.main([*base, "analizza", str(csv), "--domini", str(lavoro / "domini.toml")]) # domini corretti a mano
|
||||
for f in ("out/report.md", "out/risultati.json", "out/dominio4_T_C.png"):
|
||||
assert (lavoro / f).exists(), f
|
||||
assert "dominio 4 raffreddamento" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_cli_errori(tmp_path):
|
||||
base = ["--config", str(RADICE / "config.toml"), "--lavoro", str(tmp_path)]
|
||||
with pytest.raises(SystemExit) as e:
|
||||
cli.main([*base, "domini", str(tmp_path / "non_esiste.csv")])
|
||||
assert e.value.code == 4
|
||||
with pytest.raises(SystemExit) as e:
|
||||
cli.main(["--config", "/non/esiste.toml", "domini", "x.csv"])
|
||||
assert e.value.code == 3
|
||||
@@ -1,55 +0,0 @@
|
||||
"""Verifiche sui CSV generati da gen_data (dati/gen_<seed>.csv, config d'esempio: tau di raffreddamento nominale 15.9 s)."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from fitseg import config, dati, domini
|
||||
from fitseg.pipeline import analizza_domini
|
||||
|
||||
RADICE = Path(__file__).parent.parent
|
||||
SEEDS = [1, 2, 3]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cfg():
|
||||
return config.leggi(RADICE / "config.toml")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", SEEDS)
|
||||
def test_domini_riposo_gradini_raffreddamento(cfg, seed):
|
||||
t, serie = dati.leggi_csv(RADICE / f"dati/gen_{seed}.csv", cfg)
|
||||
tipi = [d["tipo"] for d in domini.proponi(t, serie, cfg)]
|
||||
assert tipi == ["riposo", "riscaldamento", "riscaldamento", "raffreddamento"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", SEEDS)
|
||||
def test_raffreddamento_esponenziale(cfg, seed):
|
||||
t, serie = dati.leggi_csv(RADICE / f"dati/gen_{seed}.csv", cfg)
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
for nome in serie:
|
||||
vs = ris[-1]["serie"][nome]
|
||||
assert vs["scelta"] == "esp_decad"
|
||||
fit = next(x for x in vs["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["parametri"]["tau"] == pytest.approx(15.9, rel=0.1)
|
||||
|
||||
|
||||
def test_riposo_costante(cfg):
|
||||
t, serie = dati.leggi_csv(RADICE / "dati/gen_1.csv", cfg)
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
assert ris[0]["serie"]["dT_C"]["scelta"] == "costante"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("seed", SEEDS)
|
||||
def test_riscaldamento_esponenziale_con_costanti_del_raffreddamento(cfg, seed):
|
||||
t, serie = dati.leggi_csv(RADICE / f"dati/gen_{seed}.csv", cfg)
|
||||
ris = analizza_domini(t, serie, domini.proponi(t, serie, cfg), cfg)
|
||||
for nome in serie:
|
||||
raff = next(x for x in ris[-1]["serie"][nome]["fit"] if x["modello"] == "esp_decad")["parametri"]
|
||||
for r in ris[1:3]:
|
||||
vs = r["serie"][nome]
|
||||
assert vs["scelta"] == "esp_decad"
|
||||
fit = next(x for x in vs["fit"] if x["modello"] == "esp_decad")
|
||||
assert fit["fissi"] == ["c", "tau"]
|
||||
assert fit["parametri"]["tau"] == raff["tau"] and fit["parametri"]["c"] == raff["c"]
|
||||
assert fit["parametri"]["A"] > 0
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Regola di scelta del modello (SPEC §5.3) su fit finti."""
|
||||
|
||||
from fitseg.analisi import scegli
|
||||
|
||||
|
||||
def fit(nome, k, aicc, rmse=0.1, **extra):
|
||||
base = {"modello": nome, "n_parametri": k, "aicc": aicc, "rmse": rmse, "convergenza": True,
|
||||
"parametri_a_limite": [], "non_identificabile": False}
|
||||
return {**base, **extra}
|
||||
|
||||
|
||||
def test_vince_meno_parametri_entro_delta(cfg):
|
||||
scelto, _, _ = scegli([fit("a", 1, -100.0), fit("b", 3, -101.0)], 0.1, cfg) # ΔAICc = 1 < 2
|
||||
assert scelto["modello"] == "a"
|
||||
|
||||
|
||||
def test_vince_il_migliore_oltre_delta(cfg):
|
||||
scelto, _, _ = scegli([fit("a", 1, -100.0), fit("b", 3, -110.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "b"
|
||||
|
||||
|
||||
def test_scarta_non_validi(cfg):
|
||||
fits = [fit("a", 1, -200.0, non_identificabile=True), fit("b", 2, -100.0),
|
||||
fit("c", 2, -300.0, parametri_a_limite=["tau"]), fit("d", 2, -400.0, convergenza=False)]
|
||||
assert scegli(fits, 0.1, cfg)[0]["modello"] == "b"
|
||||
|
||||
|
||||
def test_nessun_valido_indeterminato(cfg):
|
||||
scelto, motivo, adeguato = scegli([fit("a", 1, -1.0, non_identificabile=True)], 0.1, cfg)
|
||||
assert scelto is None and not adeguato and "indeterminato" in motivo
|
||||
|
||||
|
||||
def test_preferisce_i_modelli_adeguati(cfg):
|
||||
# b ha AICc molto migliore ma RMSE oltre soglia·σ; a è adeguato: vince tra gli adeguati
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, rmse=0.1), fit("b", 3, -500.0, rmse=1.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and adeguato
|
||||
|
||||
|
||||
def test_nessun_adeguato_lo_segnala(cfg):
|
||||
scelto, _, adeguato = scegli([fit("a", 1, -50.0, rmse=1.0)], 0.1, cfg)
|
||||
assert scelto["modello"] == "a" and not adeguato
|
||||
Reference in new issue
Block a user