"""T8: 4 procese, coada per fir, rate limit, plafon de cost.""" from __future__ import annotations import asyncio import pytest import limits as L def mkl(store=None, **kw): kw.setdefault("alerter", lambda *a, **k: None) return L.Limits(store, **kw) async def test_maxim_4_procese_al_cincilea_asteapta(): lim = mkl(max_procs=4) held = [] ev = asyncio.Event() async def hold(i): async with lim.process_slot(): held.append(i) await ev.wait() tasks = [asyncio.create_task(hold(i)) for i in range(5)] await asyncio.sleep(0.05) assert len(held) == 4 and lim.free_slots == 0 ev.set() await asyncio.gather(*tasks) assert len(held) == 5 and lim.free_slots == 4 async def test_coada_per_fir_serializeaza_acelasi_fir(): lim = mkl() ordine = [] async def tur(tag, delay): async with lim.thread_lock("1"): ordine.append(f"start-{tag}") await asyncio.sleep(delay) ordine.append(f"stop-{tag}") await asyncio.gather(tur("a", 0.02), tur("b", 0.0)) assert ordine == ["start-a", "stop-a", "start-b", "stop-b"] async def test_fire_diferite_merg_in_paralel(): lim = mkl() async with lim.thread_lock("1"): assert lim.queued("1") and not lim.queued("2") async with lim.thread_lock("2"): pass def test_rate_limit_per_user(): t = [1000.0] lim = mkl(rate_per_min=3, clock=lambda: t[0]) for _ in range(3): lim.admit("u1") with pytest.raises(L.RateLimited) as exc: lim.admit("u1") assert exc.value.retry_after == pytest.approx(60.0) lim.admit("u2") # alt utilizator nu e afectat t[0] += 61.0 lim.admit("u1") # fereastra a trecut def test_plafon_de_cost_opreste_botul(store): lim = mkl(store, cost_cap=1.0) assert not lim.stopped() and lim.cost_remaining() == pytest.approx(1.0) lim.record_cost(0.4, "1") lim.admit("u1") assert lim.record_cost(0.7, "1") == pytest.approx(1.1) assert lim.stopped() and lim.cost_remaining() == 0.0 with pytest.raises(L.CostCapReached): lim.admit("u1") def test_plafonul_off_nu_opreste_nimic(store, state_dir): import config (state_dir / "env").write_text("COST_CAP_USD_DAY=off\n") config.reload(state_dir) alerte = [] lim = L.Limits(store, alerter=lambda *a, **k: alerte.append(a)) assert lim.cost_cap == 0.0 and not lim.cap_enabled lim.record_cost(99.0, "1") assert not lim.stopped() and lim.cost_remaining() == float("inf") lim.admit("u1") # nu ridica CostCapReached assert alerte == [] @pytest.mark.parametrize("raw, asteptat", [ ("off", 0.0), ("", 0.0), ("nelimitat", 0.0), ("0", 0.0), ("-3", 0.0), ("5.00", 5.0), (" 2.5 ", 2.5), ("gunoi", 0.0), (None, 0.0), ]) def test_parse_cap(raw, asteptat): assert L.parse_cap(raw, 0.0) == pytest.approx(asteptat) def test_plafon_alerteaza_o_singura_data(store): alerte = [] lim = mkl(store, cost_cap=0.5, alerter=lambda *a, **k: alerte.append(a)) lim.record_cost(0.6, "1") lim.record_cost(0.1, "1") assert len(alerte) == 1 and alerte[0][0] == "CRITICAL" def test_plafonul_se_ridica_a_doua_zi(store): lim = mkl(store, cost_cap=1.0) lim.record_cost(2.0, "1") assert lim.stopped() store.state["cost"]["day"] = "2000-01-01" # ziua se schimba assert not lim.stopped() def test_costul_fara_store_e_local(): lim = mkl(None, cost_cap=1.0) lim.record_cost(0.5) lim.record_cost("gunoi") assert lim.cost_today() == pytest.approx(0.5) def test_valorile_implicite_vin_din_config(state_dir): import config (state_dir / "env").write_text("COST_CAP_USD_DAY=2.5\nMAX_PROCS=2 # comentariu\n") config.reload(state_dir) lim = mkl() assert lim.cost_cap == 2.5 and lim.max_procs == 2 assert lim.turn_timeout == 900.0 async def test_contextul_turn_verifica_tot(store): lim = mkl(store, max_procs=1, cost_cap=10.0, turn_timeout=42.0) async with lim.turn("1", "u1") as timeout: assert timeout == 42.0 assert lim.free_slots == 0 assert lim.free_slots == 1