Files
MajoorWaldi 4411c72e4e Refactor test imports and clean up unused code across multiple test files
- Removed unnecessary imports from various test files to improve readability and maintainability.
- Added new settings for the Generated Feed display in the settingsFeed.js file.
- Introduced a new configuration file for Pyright type checking.
2026-03-28 13:26:28 +01:00

148 lines
4.7 KiB
Python

"""
Pytest-based code quality gates using radon.
These tests enforce complexity thresholds so regressions are caught
during normal ``pytest`` runs, not just in the CI quality job.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
BACKEND_PACKAGES = ["mjr_am_backend", "mjr_am_shared"]
# Maximum cyclomatic complexity per function/method.
# Some existing parser/service functions reach CC ~21-25; keep the gate
# at the current max to prevent regressions without failing on
# already-shipped code. Gradually lower this as complex functions are
# refactored.
MAX_CC_PER_FUNCTION = 25
# Maximum average complexity across all analysed modules.
MAX_AVG_COMPLEXITY = 5.0
# Minimum maintainability index (radon mi) — "A" grade is >= 20.
MIN_MAINTAINABILITY_INDEX = 15.0
def _existing_packages() -> list[str]:
return [p for p in BACKEND_PACKAGES if (REPO_ROOT / p).is_dir()]
def _run_radon(subcommand: str, *extra_args: str) -> str:
packages = _existing_packages()
if not packages:
pytest.skip("No backend packages found")
cmd = [sys.executable, "-m", "radon", subcommand, *extra_args, *packages]
result = subprocess.run(
cmd, cwd=str(REPO_ROOT), capture_output=True, text=True, timeout=120,
)
return result.stdout.strip()
# -- Cyclomatic Complexity ---------------------------------------------------
def _parse_cc_json() -> dict:
raw = _run_radon("cc", "--json", "--no-assert")
if not raw:
return {}
return json.loads(raw)
def test_no_function_exceeds_max_cc():
"""Every function/method must have CC <= MAX_CC_PER_FUNCTION."""
data = _parse_cc_json()
violations: list[str] = []
for _filepath, items in data.items():
if not isinstance(items, list):
continue
for item in items:
if not isinstance(item, dict):
continue
cc = int(item.get("complexity") or 0)
if cc > MAX_CC_PER_FUNCTION:
name = item.get("name", "<unknown>")
lineno = item.get("lineno", "?")
violations.append(f" {_filepath}:{lineno} {name} (CC={cc})")
assert not violations, (
f"Functions exceeding CC>{MAX_CC_PER_FUNCTION}:\n" + "\n".join(violations)
)
def test_average_complexity_within_threshold():
"""Average CC across all backend modules must stay below threshold."""
data = _parse_cc_json()
total_cc = 0
count = 0
for _filepath, items in data.items():
if not isinstance(items, list):
continue
for item in items:
if not isinstance(item, dict):
continue
total_cc += int(item.get("complexity") or 0)
count += 1
if count == 0:
pytest.skip("No functions analysed")
avg = total_cc / count
assert avg <= MAX_AVG_COMPLEXITY, (
f"Average CC is {avg:.2f}, exceeds threshold {MAX_AVG_COMPLEXITY}"
)
# -- Maintainability Index ---------------------------------------------------
def _parse_mi_json() -> dict:
raw = _run_radon("mi", "--json")
if not raw:
return {}
return json.loads(raw)
def test_no_module_below_min_maintainability():
"""Every module must have a maintainability index >= MIN_MAINTAINABILITY_INDEX."""
data = _parse_mi_json()
violations: list[str] = []
for filepath, mi_score in data.items():
if not isinstance(mi_score, (int, float)):
continue
if mi_score < MIN_MAINTAINABILITY_INDEX:
violations.append(f" {filepath} (MI={mi_score:.1f})")
assert not violations, (
f"Modules below MI<{MIN_MAINTAINABILITY_INDEX}:\n" + "\n".join(violations)
)
# -- Raw Metrics Summary (informational, always passes) ----------------------
def test_raw_metrics_summary():
"""Collect and print raw metrics for visibility — always passes."""
data = _parse_cc_json()
total_functions = 0
total_cc = 0
max_cc = 0
max_cc_name = ""
for filepath, items in data.items():
if not isinstance(items, list):
continue
for item in items:
if not isinstance(item, dict):
continue
cc = int(item.get("complexity") or 0)
total_functions += 1
total_cc += cc
if cc > max_cc:
max_cc = cc
max_cc_name = f"{filepath}:{item.get('lineno', '?')} {item.get('name', '?')}"
if total_functions:
avg = total_cc / total_functions
print(f"\n[Quality] {total_functions} functions, avg CC={avg:.2f}, max CC={max_cc} ({max_cc_name})")
else:
print("\n[Quality] No functions analysed")