"""Step definitions for large-project scaling performance validation.""" from __future__ import annotations import json import math from pathlib import Path from typing import Any from behave import given, then, when from behave.runner import Context _FIXTURES_DIR = Path(__file__).resolve().parents[1] / "fixtures" / "scale" # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("a scaling performance test environment is ready") def step_scaling_perf_env_ready(context: Context) -> None: """Verify scale test fixtures directory exists.""" assert _FIXTURES_DIR.is_dir(), f"Fixtures dir missing: {_FIXTURES_DIR}" context.sp_metadata = None context.sp_thresholds = None # --------------------------------------------------------------------------- # Fixture loading # --------------------------------------------------------------------------- @when("I scaling perf load the scale metadata fixture") def step_scaling_perf_load_metadata(context: Context) -> None: path = _FIXTURES_DIR / "scale_metadata.json" assert path.exists(), f"Missing fixture: {path}" with open(path) as fh: context.sp_metadata = json.load(fh) @when("I scaling perf load the baseline thresholds fixture") def step_scaling_perf_load_thresholds(context: Context) -> None: path = _FIXTURES_DIR / "baseline_thresholds.json" assert path.exists(), f"Missing fixture: {path}" with open(path) as fh: context.sp_thresholds = json.load(fh) # --------------------------------------------------------------------------- # Profile validation # --------------------------------------------------------------------------- def _get_profile(context: Context, name: str) -> dict[str, Any]: """Return the profile dict with the given name, or fail.""" assert context.sp_metadata is not None, "Metadata not loaded" for p in context.sp_metadata["scale_profiles"]: if p["name"] == name: return p raise AssertionError(f"Profile {name!r} not found in metadata") @then('the scaling perf metadata should contain profile "{name}"') def step_scaling_perf_has_profile(context: Context, name: str) -> None: _get_profile(context, name) @then('the scaling perf profile "{name}" should have file count {count:d}') def step_scaling_perf_profile_file_count( context: Context, name: str, count: int ) -> None: profile = _get_profile(context, name) actual = profile["file_count"] assert actual == count, f"Profile {name}: expected {count}, got {actual}" @then('the scaling perf language mix should sum to 1.0 for profile "{name}"') def step_scaling_perf_lang_mix_sums(context: Context, name: str) -> None: profile = _get_profile(context, name) total = sum(profile["language_mix"].values()) assert math.isclose(total, 1.0, rel_tol=1e-6), ( f"Language mix for {name} sums to {total}, expected 1.0" ) # --------------------------------------------------------------------------- # Threshold coverage # --------------------------------------------------------------------------- @then('the scaling perf indexing thresholds should cover "{file_count}"') def step_scaling_perf_indexing_covers(context: Context, file_count: str) -> None: assert context.sp_thresholds is not None assert file_count in context.sp_thresholds["indexing"], ( f"Missing indexing threshold for {file_count}" ) @then('the scaling perf decomposition thresholds should cover "{file_count}"') def step_scaling_perf_decomp_covers(context: Context, file_count: str) -> None: assert context.sp_thresholds is not None assert file_count in context.sp_thresholds["decomposition"], ( f"Missing decomposition threshold for {file_count}" ) # --------------------------------------------------------------------------- # Monotonicity # --------------------------------------------------------------------------- @then('scaling perf p50 should be less than p95 for "{key}" indexing') def step_scaling_perf_p50_lt_p95(context: Context, key: str) -> None: assert context.sp_thresholds is not None t = context.sp_thresholds["indexing"][key] assert t["p50_ms"] < t["p95_ms"], ( f"p50 ({t['p50_ms']}) >= p95 ({t['p95_ms']}) for {key}" ) @then('scaling perf p95 should be less than p99 for "{key}" indexing') def step_scaling_perf_p95_lt_p99(context: Context, key: str) -> None: assert context.sp_thresholds is not None t = context.sp_thresholds["indexing"][key] assert t["p95_ms"] < t["p99_ms"], ( f"p95 ({t['p95_ms']}) >= p99 ({t['p99_ms']}) for {key}" ) # --------------------------------------------------------------------------- # Sub-linear scaling checks # --------------------------------------------------------------------------- @then("scaling perf 50K indexing p50 should be less than 5x the 10K p50") def step_scaling_perf_50k_sublinear(context: Context) -> None: assert context.sp_thresholds is not None idx = context.sp_thresholds["indexing"] p50_10k = idx["10000_files"]["p50_ms"] p50_50k = idx["50000_files"]["p50_ms"] assert p50_50k < 5 * p50_10k, ( f"50K p50 ({p50_50k}) should be < 5x 10K p50 ({5 * p50_10k})" ) @then("scaling perf 100K indexing p50 should be less than 2.5x the 50K p50") def step_scaling_perf_100k_sublinear(context: Context) -> None: assert context.sp_thresholds is not None idx = context.sp_thresholds["indexing"] p50_50k = idx["50000_files"]["p50_ms"] p50_100k = idx["100000_files"]["p50_ms"] limit = int(2.5 * p50_50k) assert p50_100k < limit, f"100K p50 ({p50_100k}) should be < 2.5x 50K p50 ({limit})" # --------------------------------------------------------------------------- # Context assembly # --------------------------------------------------------------------------- @then("the scaling perf thresholds should have context assembly section") def step_scaling_perf_has_ctx_assembly(context: Context) -> None: assert context.sp_thresholds is not None assert "context_assembly" in context.sp_thresholds @then('the scaling perf context assembly thresholds should cover "{fragment_count}"') def step_scaling_perf_ctx_assembly_covers( context: Context, fragment_count: str ) -> None: assert context.sp_thresholds is not None assert fragment_count in context.sp_thresholds["context_assembly"], ( f"Missing context_assembly threshold for {fragment_count}" ) # --------------------------------------------------------------------------- # Execution throughput # --------------------------------------------------------------------------- @then("the scaling perf thresholds should have execution throughput section") def step_scaling_perf_has_exec_throughput(context: Context) -> None: assert context.sp_thresholds is not None assert "execution_throughput" in context.sp_thresholds @then('the scaling perf execution thresholds should cover "{plan_count}"') def step_scaling_perf_exec_covers(context: Context, plan_count: str) -> None: assert context.sp_thresholds is not None assert plan_count in context.sp_thresholds["execution_throughput"], ( f"Missing execution_throughput threshold for {plan_count}" ) # --------------------------------------------------------------------------- # Memory thresholds # --------------------------------------------------------------------------- @then('the scaling perf memory thresholds should cover "{file_count}"') def step_scaling_perf_memory_covers(context: Context, file_count: str) -> None: assert context.sp_thresholds is not None assert file_count in context.sp_thresholds["memory_usage_mb"], ( f"Missing memory_usage_mb threshold for {file_count}" ) @then('scaling perf memory peak should exceed steady state for "{file_count}"') def step_scaling_perf_memory_peak_gt_steady(context: Context, file_count: str) -> None: assert context.sp_thresholds is not None mem = context.sp_thresholds["memory_usage_mb"][file_count] assert mem["peak_mb"] > mem["steady_state_mb"], ( f"Peak ({mem['peak_mb']}) <= steady ({mem['steady_state_mb']}) for {file_count}" )