from __future__ import annotations import importlib.util from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] POLICY_PATH = ROOT / "ops" / "gitea-deploy-broker" / "runner_capacity_policy.py" @pytest.fixture def policy_module(): spec = importlib.util.spec_from_file_location("appops_runner_capacity_policy_test", POLICY_PATH) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _state(policy_module, **overrides): values = { "queue_depth": 0, "oldest_queued_seconds": None, "load_ratio": 0.30, "available_memory_bytes": 20 * policy_module.GIB, "burst_running": False, "burst_busy": False, "burst_disabled": True, "seconds_since_demand": 600, "seconds_since_action": 600, "swap_critical": False, "cache_free_bytes": 100 * policy_module.GIB, } values.update(overrides) return policy_module.BurstCapacityInput(**values) def test_burst_starts_only_for_queue_demand_with_headroom(policy_module): decision = policy_module.decide_burst_capacity( _state(policy_module, queue_depth=2) ) assert decision.action == "start" assert decision.reason == "queue_pressure_with_host_headroom" assert decision.pressure == "headroom" def test_old_job_can_trigger_burst_even_with_single_queued_job(policy_module): decision = policy_module.decide_burst_capacity( _state(policy_module, queue_depth=1, oldest_queued_seconds=60) ) assert decision.action == "start" @pytest.mark.parametrize( ("load_ratio", "memory_gib", "swap_critical"), [ (0.75, 20, False), (0.30, 10, False), (0.30, 20, True), ], ) def test_burst_does_not_start_without_headroom( policy_module, load_ratio, memory_gib, swap_critical ): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=3, load_ratio=load_ratio, available_memory_bytes=memory_gib * policy_module.GIB, swap_critical=swap_critical, ) ) assert decision.action == "hold" assert decision.reason == "queue_pressure_but_host_headroom_insufficient" def test_high_pressure_drains_busy_burst_instead_of_stopping_it(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=3, load_ratio=0.95, burst_running=True, burst_busy=True, burst_disabled=False, ) ) assert decision.action == "drain" assert decision.reason == "host_pressure_drain_after_current_job" def test_high_pressure_stops_only_already_drained_idle_burst(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, load_ratio=0.95, burst_running=True, burst_busy=False, burst_disabled=True, seconds_since_action=600, ) ) assert decision.action == "stop" assert decision.reason == "host_pressure_idle_burst_can_stop" def test_idle_burst_is_drained_before_stop(policy_module): first = policy_module.decide_burst_capacity( _state( policy_module, burst_running=True, burst_disabled=False, seconds_since_demand=600, seconds_since_action=600, ) ) second = policy_module.decide_burst_capacity( _state( policy_module, burst_running=True, burst_disabled=True, seconds_since_demand=600, seconds_since_action=600, ) ) assert first.action == "drain" assert second.action == "stop" def test_disabled_running_burst_reenables_when_demand_returns(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=2, burst_running=True, burst_disabled=True, seconds_since_action=600, ) ) assert decision.action == "enable" def test_start_cooldown_prevents_flapping(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=2, seconds_since_action=30, ) ) assert decision.action == "hold" def test_burst_does_not_start_when_cache_headroom_is_low(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=3, cache_free_bytes=20 * policy_module.GIB, ) ) assert decision.action == "hold" assert decision.reason == "queue_pressure_but_host_headroom_insufficient" def test_low_cache_headroom_drains_busy_burst(policy_module): decision = policy_module.decide_burst_capacity( _state( policy_module, queue_depth=3, burst_running=True, burst_busy=True, burst_disabled=False, cache_free_bytes=10 * policy_module.GIB, ) ) assert decision.action == "drain" assert decision.pressure == "critical"