diff --git a/airdef-mod/tests/budgets/wasm_budget.toml b/airdef-mod/tests/budgets/wasm_budget.toml index fc0939e..3fa8027 100644 --- a/airdef-mod/tests/budgets/wasm_budget.toml +++ b/airdef-mod/tests/budgets/wasm_budget.toml @@ -262,17 +262,31 @@ expect.feedback = "Тор-М2 развёрнут (integrated, multi)" command = "airdef deploy s400" expect.feedback = "С-400 развёрнут (integrated, multi)" -# Two maximum supported waves are issued before simulation advances, so -# their 160 airborne units overlap while 64 defensive systems track them. -[[step]] -command = "airdef wave mix 80 4000" - +# The next raid can only be ordered after the first plan is complete. [[step]] command = "airdef wave kh101 80 4000" +[[step]] +ticks = 450 +frames = 225 +expect.chat = ["Противник готовит налёт: ищет площадки, местность подгружается", "Площадок за пределами загруженной карты: 1: оттуда пуск без площадки, с высоты полёта", "ВОЗДУШНАЯ ТРЕВОГА: налёт, 80 целей (один сектор)"] + +[[step]] +command = "airdef wave kh101 80 4000" +expect.chat = [] + [[step]] ticks = 600 frames = 300 +expect.chat = ["Противник готовит налёт: ищет площадки, местность подгружается", "Площадок за пределами загруженной карты: 1: оттуда пуск без площадки, с высоты полёта", "ВОЗДУШНАЯ ТРЕВОГА: налёт, 80 целей (один сектор)"] + +[[step]] +command = "airdef stats" +expect.feedback = "В воздухе: 12, ракет: 0, ПВО: 64, сбито: 0, поражений: 0, разбились о препятствия: 0; удары по ПУ: пущено 0, уничтожено 0, подавлено 0, промахов 0, сбито ПВО 0; мир: 0 секций (0 КБ), запросов 652, неизвестно 0, лучей 5642 (ударов 0, неизв. 5642), клеток 5642; взрывы в блоках: 0 (подтверждено 0, без перемен 0, нет чанка 0), чужих ответов 0" + +[[step]] +ticks = 400 +frames = 200 [[step]] command = "airdef clear" diff --git a/tools/airdef.py b/tools/airdef.py index 7eac942..449be7b 100644 --- a/tools/airdef.py +++ b/tools/airdef.py @@ -167,10 +167,10 @@ def test_budgets(): "scenario": "tests/budgets/wasm_budget.toml", "status": "failed", "exit_code": None, - "scenario_counts": {"scripts": 1, "steps": None, "raids": 2, - "max_units_per_raid": 80, "raid_units": 160, - "air_defense_units": None, "layout_unit_limit": None, - "frames": 300, "ticks": 602}, + "scenario_counts": {"scripts": 1, "steps": None, "raid_requests": None, + "raid_units_requested": None, "air_defense_units": None, + "layout_unit_limit": None, "frames": None, "ticks": None, + "accepted_raids_observed": None, "peak_airborne_observed": None}, "runtime_limits": {"source": "horizon/crates/horizon-mods/src/runtime.rs", "metering": "source_configured; measured_per_scenario: unavailable"}, } @@ -206,8 +206,15 @@ def test_budgets(): wasmtime = re.search(r'\[\[package\]\]\s*name = "wasmtime"\s*version = "([^"]+)"', lock_text) report["wasmtime_version"] = wasmtime.group(1) if wasmtime else None fixture = ROOT / "airdef-mod" / "tests" / "budgets" / "wasm_budget.toml" - report["scenario_counts"]["steps"] = len(re.findall(r"(?m)^\[\[step\]\]$", fixture.read_text())) - report["scenario_counts"]["air_defense_units"] = len(re.findall(r'(?m)^command = "airdef deploy [^"]+"', fixture.read_text())) + fixture_text = fixture.read_text() + counts = report["scenario_counts"] + counts["steps"] = len(re.findall(r"(?m)^\[\[step\]\]$", fixture_text)) + counts["air_defense_units"] = len(re.findall(r'(?m)^command = "airdef deploy [^"]+"', fixture_text)) + requests = re.findall(r'(?m)^command = "airdef wave \S+ (\d+)(?: [^"]*)?"', fixture_text) + counts["raid_requests"] = len(requests) + counts["raid_units_requested"] = sum(map(int, requests)) + for field in ("ticks", "frames"): + counts[field] = sum(map(int, re.findall(rf"(?m)^{field}\s*=\s*(\d+)\s*$", fixture_text))) report["scenario_sha256"] = hashlib.sha256(fixture.read_bytes()).hexdigest() mod() diff --git a/tools/test_budget_report.py b/tools/test_budget_report.py index c4788e7..9d7abd7 100644 --- a/tools/test_budget_report.py +++ b/tools/test_budget_report.py @@ -94,6 +94,21 @@ class BudgetReportFailureTests(unittest.TestCase): self.assertEqual(report["status"], "failed") return report + def test_counts_are_requests_and_derived_from_fixture(self): + (self.mod_root / "tests/budgets/wasm_budget.toml").write_text( + '[[step]]\ncommand = "airdef deploy s400"\n' + '[[step]]\ncommand = "airdef wave kh101 80 4000"\n' + '[[step]]\nticks = 450\nframes = 225\n' + '[[step]]\ncommand = "airdef wave mix 40 1500"\n' + ) + with mock.patch.object(airdef, "mod", side_effect=SystemExit("stop after provenance")): + counts = self.invoke_and_require_failure()["scenario_counts"] + self.assertEqual((counts["steps"], counts["air_defense_units"]), (4, 1)) + self.assertEqual((counts["raid_requests"], counts["raid_units_requested"]), (2, 120)) + self.assertEqual((counts["ticks"], counts["frames"]), (450, 225)) + self.assertIsNone(counts["accepted_raids_observed"]) + self.assertIsNone(counts["peak_airborne_observed"]) + def test_early_system_exit_writes_report_and_skips_backend(self): backend = mock.Mock() with mock.patch.object(airdef, "mod", side_effect=SystemExit("synthetic build failure")), \