Harden runtime compatibility policy checks
Some checks failed
Tests / Build and Push CICD Base Image (pull_request) Successful in 1h37m28s
Tests / Build and Push CICD Complete Image (pull_request) Failing after 1h57m45s
Tests / Trailing Whitespace Check (pull_request) Has been skipped
Tests / End of File Check (pull_request) Has been skipped
Tests / YAML Syntax Check (pull_request) Has been skipped
Tests / TOML Syntax Check (pull_request) Has been skipped
Tests / Mixed Line Ending Check (pull_request) Has been skipped
Tests / TOML Formatting Check (pull_request) Has been skipped
Tests / Ruff Linting (pull_request) Has been skipped
Tests / Ruff Format Check (pull_request) Has been skipped
Tests / Pyright Type Check (pull_request) Has been skipped
Tests / Darglint Docstring Check (pull_request) Has been skipped
Tests / No Docstring Types Check (pull_request) Has been skipped
Tests / ESLint Check (pull_request) Has been skipped
Tests / Prettier Format Check (pull_request) Has been skipped
Tests / TypeScript Type Check (pull_request) Has been skipped
Tests / TSDoc Lint Check (pull_request) Has been skipped
Tests / Backend Tests (pull_request) Has been skipped
Tests / Frontend Tests (pull_request) Has been skipped
Tests / Backend Doctests (pull_request) Has been skipped
Tests / Integration Tests (pull_request) Has been skipped
Tests / End-to-End Tests (pull_request) Has been skipped
Some checks failed
Tests / Build and Push CICD Base Image (pull_request) Successful in 1h37m28s
Tests / Build and Push CICD Complete Image (pull_request) Failing after 1h57m45s
Tests / Trailing Whitespace Check (pull_request) Has been skipped
Tests / End of File Check (pull_request) Has been skipped
Tests / YAML Syntax Check (pull_request) Has been skipped
Tests / TOML Syntax Check (pull_request) Has been skipped
Tests / Mixed Line Ending Check (pull_request) Has been skipped
Tests / TOML Formatting Check (pull_request) Has been skipped
Tests / Ruff Linting (pull_request) Has been skipped
Tests / Ruff Format Check (pull_request) Has been skipped
Tests / Pyright Type Check (pull_request) Has been skipped
Tests / Darglint Docstring Check (pull_request) Has been skipped
Tests / No Docstring Types Check (pull_request) Has been skipped
Tests / ESLint Check (pull_request) Has been skipped
Tests / Prettier Format Check (pull_request) Has been skipped
Tests / TypeScript Type Check (pull_request) Has been skipped
Tests / TSDoc Lint Check (pull_request) Has been skipped
Tests / Backend Tests (pull_request) Has been skipped
Tests / Frontend Tests (pull_request) Has been skipped
Tests / Backend Doctests (pull_request) Has been skipped
Tests / Integration Tests (pull_request) Has been skipped
Tests / End-to-End Tests (pull_request) Has been skipped
This commit is contained in:
@@ -15,12 +15,22 @@ REQUIRED_PACKAGE_PINS: dict[str, str] = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_required_python(required_python: str) -> tuple[int, int] | None:
|
||||||
|
"""Parse required Python version from strict major.minor format."""
|
||||||
|
value = required_python.strip()
|
||||||
|
pieces = value.split(".")
|
||||||
|
if len(pieces) != 2 or not all(piece.isdigit() for piece in pieces):
|
||||||
|
return None
|
||||||
|
return int(pieces[0]), int(pieces[1])
|
||||||
|
|
||||||
|
|
||||||
def _runtime_meets_required_python(required_python: str) -> bool:
|
def _runtime_meets_required_python(required_python: str) -> bool:
|
||||||
"""Return True when the interpreter version satisfies required major.minor."""
|
"""Return True when the interpreter major.minor exactly matches required policy."""
|
||||||
major_str, minor_str = required_python.split(".", maxsplit=1)
|
required = _parse_required_python(required_python)
|
||||||
required = (int(major_str), int(minor_str))
|
if required is None:
|
||||||
|
return False
|
||||||
current = (sys.version_info.major, sys.version_info.minor)
|
current = (sys.version_info.major, sys.version_info.minor)
|
||||||
return current >= required
|
return current == required
|
||||||
|
|
||||||
|
|
||||||
def _installed_version(package_name: str) -> str:
|
def _installed_version(package_name: str) -> str:
|
||||||
@@ -32,22 +42,32 @@ def compatibility_status() -> dict[str, object]:
|
|||||||
"""Compute runtime and pinned-dependency compatibility status."""
|
"""Compute runtime and pinned-dependency compatibility status."""
|
||||||
required_python = os.getenv("BACKEND_REQUIRED_PYTHON", "3.14")
|
required_python = os.getenv("BACKEND_REQUIRED_PYTHON", "3.14")
|
||||||
current_python = f"{sys.version_info.major}.{sys.version_info.minor}"
|
current_python = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||||
|
required_python_parsed = _parse_required_python(required_python)
|
||||||
|
|
||||||
package_checks: dict[str, bool] = {}
|
package_checks: dict[str, bool] = {}
|
||||||
|
package_errors: dict[str, str] = {}
|
||||||
for package_name, required_version in REQUIRED_PACKAGE_PINS.items():
|
for package_name, required_version in REQUIRED_PACKAGE_PINS.items():
|
||||||
package_checks[package_name] = (
|
try:
|
||||||
_installed_version(package_name) == required_version
|
package_checks[package_name] = (
|
||||||
)
|
_installed_version(package_name) == required_version
|
||||||
|
)
|
||||||
|
except metadata.PackageNotFoundError as exc:
|
||||||
|
package_checks[package_name] = False
|
||||||
|
package_errors[package_name] = str(exc)
|
||||||
|
|
||||||
runtime_ok = _runtime_meets_required_python(required_python)
|
runtime_ok = required_python_parsed is not None and _runtime_meets_required_python(
|
||||||
|
required_python
|
||||||
|
)
|
||||||
packages_ok = all(package_checks.values())
|
packages_ok = all(package_checks.values())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"ok": runtime_ok and packages_ok,
|
"ok": runtime_ok and packages_ok,
|
||||||
"required_python": required_python,
|
"required_python": required_python,
|
||||||
"current_python": current_python,
|
"current_python": current_python,
|
||||||
|
"python_policy_valid": required_python_parsed is not None,
|
||||||
"required_packages": REQUIRED_PACKAGE_PINS,
|
"required_packages": REQUIRED_PACKAGE_PINS,
|
||||||
"package_checks": package_checks,
|
"package_checks": package_checks,
|
||||||
|
"package_errors": package_errors,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"""Integration tests for API endpoints."""
|
"""Integration tests for API endpoints."""
|
||||||
|
|
||||||
|
from importlib import metadata
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from backend.main import app
|
from backend.main import app, compatibility_status
|
||||||
|
|
||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
@@ -49,8 +51,42 @@ class TestAPIIntegration:
|
|||||||
assert payload["ok"] is True
|
assert payload["ok"] is True
|
||||||
assert payload["required_python"] == "3.14"
|
assert payload["required_python"] == "3.14"
|
||||||
assert "current_python" in payload
|
assert "current_python" in payload
|
||||||
|
assert payload["python_policy_valid"] is True
|
||||||
assert "required_packages" in payload
|
assert "required_packages" in payload
|
||||||
assert payload["required_packages"]["fastapi"] == "0.120.2"
|
assert payload["required_packages"]["fastapi"] == "0.120.2"
|
||||||
assert payload["required_packages"]["uvicorn"] == "0.38.0"
|
assert payload["required_packages"]["uvicorn"] == "0.38.0"
|
||||||
|
assert payload["package_errors"] == {}
|
||||||
|
|
||||||
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
|
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
|
||||||
|
|
||||||
|
def test_startup_rejects_malformed_required_python(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Startup should fail fast when Python policy is not strict major.minor."""
|
||||||
|
monkeypatch.setenv("BACKEND_REQUIRED_PYTHON", "3.14.1")
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError), TestClient(app):
|
||||||
|
pass
|
||||||
|
|
||||||
|
monkeypatch.delenv("BACKEND_REQUIRED_PYTHON", raising=False)
|
||||||
|
|
||||||
|
def test_compatibility_status_handles_missing_package_metadata(
|
||||||
|
self, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
"""Compatibility checks should handle missing package metadata gracefully."""
|
||||||
|
|
||||||
|
def raise_not_found(_: str) -> str:
|
||||||
|
raise metadata.PackageNotFoundError("fake")
|
||||||
|
|
||||||
|
monkeypatch.setattr("backend.main._installed_version", raise_not_found)
|
||||||
|
status = compatibility_status()
|
||||||
|
package_checks = status["package_checks"]
|
||||||
|
package_errors = status["package_errors"]
|
||||||
|
|
||||||
|
assert status["ok"] is False
|
||||||
|
assert isinstance(package_checks, dict)
|
||||||
|
assert isinstance(package_errors, dict)
|
||||||
|
assert package_checks["fastapi"] is False
|
||||||
|
assert package_checks["uvicorn"] is False
|
||||||
|
assert "fastapi" in package_errors
|
||||||
|
assert "uvicorn" in package_errors
|
||||||
|
|||||||
Reference in New Issue
Block a user