forked from HAL9000/cleveragents-core
434cad5d1a
- Normalize import ordering and remove unused imports - Replace non-ASCII dashes in comments with ASCII hyphens - Clean up unused assignments and use contextlib.suppress - Adjust unused variable name in plan_cli_streaming steps
391 lines
13 KiB
Python
391 lines
13 KiB
Python
"""Step definitions for ADR compliance checker script coverage.
|
|
|
|
Covers every line and branch in ``scripts/check-adr-compliance.py``:
|
|
- check_adr002_async_model
|
|
- check_adr003_dependency_injection
|
|
- check_adr007_repository_pattern
|
|
- main()
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from behave import given, then, when
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Module loader - the script uses hyphens so we need importlib
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _load_adr_module():
|
|
spec = importlib.util.spec_from_file_location(
|
|
"check_adr_compliance", "scripts/check-adr-compliance.py"
|
|
)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
_adr = _load_adr_module()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _make_tmp_src(context) -> Path:
|
|
"""Create a fresh temp directory and return a ``src_dir`` Path inside it."""
|
|
context.tmp_root = tempfile.mkdtemp()
|
|
src_dir = Path(context.tmp_root) / "src"
|
|
src_dir.mkdir()
|
|
context.src_dir = src_dir
|
|
return src_dir
|
|
|
|
|
|
def _cleanup(context):
|
|
"""Remove the temp tree if it exists."""
|
|
root = getattr(context, "tmp_root", None)
|
|
if root and os.path.isdir(root):
|
|
shutil.rmtree(root)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Givens - ADR-002
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a temporary source directory with no application subdirectory")
|
|
def given_no_app_dir(context):
|
|
_make_tmp_src(context)
|
|
# deliberately do NOT create an "application" subdirectory
|
|
|
|
|
|
@given("a temporary source directory with a clean application module")
|
|
def given_clean_app_module(context):
|
|
src = _make_tmp_src(context)
|
|
app_dir = src / "application"
|
|
app_dir.mkdir(parents=True)
|
|
(app_dir / "clean.py").write_text("import asyncio\n")
|
|
|
|
|
|
@given(
|
|
"a temporary source directory with an application module importing threading directly"
|
|
)
|
|
def given_app_import_threading(context):
|
|
src = _make_tmp_src(context)
|
|
app_dir = src / "application"
|
|
app_dir.mkdir(parents=True)
|
|
(app_dir / "bad.py").write_text("import threading\n")
|
|
|
|
|
|
@given(
|
|
"a temporary source directory with an application module using from-threading-import"
|
|
)
|
|
def given_app_from_threading(context):
|
|
src = _make_tmp_src(context)
|
|
app_dir = src / "application"
|
|
app_dir.mkdir(parents=True)
|
|
(app_dir / "bad.py").write_text("from threading import Thread\n")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Givens - ADR-003
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a temporary source directory with no services subdirectory")
|
|
def given_no_services_dir(context):
|
|
src = _make_tmp_src(context)
|
|
app_dir = src / "application"
|
|
app_dir.mkdir(parents=True)
|
|
# no "services" subdirectory
|
|
|
|
|
|
@given("a temporary source directory with only an __init__.py in services")
|
|
def given_dunder_init_only(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "__init__.py").write_text("")
|
|
|
|
|
|
@given(
|
|
"a temporary source directory with a syntactically invalid Python file in services"
|
|
)
|
|
def given_syntax_error_file(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "broken.py").write_text("def foo(\n")
|
|
|
|
|
|
@given("a temporary source directory with a service class whose init takes only self")
|
|
def given_class_init_no_deps(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "my_service.py").write_text(
|
|
"class MyService:\n def __init__(self):\n pass\n"
|
|
)
|
|
|
|
|
|
@given(
|
|
"a temporary source directory with a service class whose init takes self and a dependency"
|
|
)
|
|
def given_class_init_with_dep(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "good_service.py").write_text(
|
|
"class GoodService:\n def __init__(self, repo):\n self.repo = repo\n"
|
|
)
|
|
|
|
|
|
@given("a temporary source directory with a service class that has no init method")
|
|
def given_class_no_init(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "util_service.py").write_text(
|
|
"class UtilService:\n def do_work(self):\n pass\n"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Givens - ADR-007
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a temporary source directory with no services subdirectory for ADR-007")
|
|
def given_no_services_dir_adr007(context):
|
|
src = _make_tmp_src(context)
|
|
app_dir = src / "application"
|
|
app_dir.mkdir(parents=True)
|
|
# no "services" subdirectory
|
|
|
|
|
|
@given("a temporary source directory with a service file importing from sqlalchemy")
|
|
def given_sqlalchemy_import(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "bad_svc.py").write_text("from sqlalchemy import Column\n")
|
|
|
|
|
|
@given("a temporary source directory with a service file using session.query")
|
|
def given_session_query(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "bad_svc.py").write_text(
|
|
"def fetch(session):\n return session.query(Model).all()\n"
|
|
)
|
|
|
|
|
|
@given("a temporary source directory with a service file using session.execute")
|
|
def given_session_execute(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "bad_svc.py").write_text(
|
|
"def run(session):\n session.execute(stmt)\n"
|
|
)
|
|
|
|
|
|
@given("a temporary source directory with a clean service file using no SQLAlchemy")
|
|
def given_clean_service(context):
|
|
src = _make_tmp_src(context)
|
|
svc_dir = src / "application" / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(svc_dir / "clean_svc.py").write_text(
|
|
"class CleanService:\n def run(self):\n return 42\n"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Givens - main()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@given("a temporary project tree with clean source code for ADR compliance")
|
|
def given_clean_project_tree(context):
|
|
context.tmp_root = tempfile.mkdtemp()
|
|
src = Path(context.tmp_root) / "src" / "cleveragents"
|
|
app_dir = src / "application"
|
|
svc_dir = app_dir / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(app_dir / "clean.py").write_text("import asyncio\n")
|
|
(svc_dir / "good.py").write_text(
|
|
"class Good:\n def __init__(self, dep):\n self.dep = dep\n"
|
|
)
|
|
context.project_root = context.tmp_root
|
|
|
|
|
|
@given("a temporary project tree with ADR violations in the source code")
|
|
def given_violating_project_tree(context):
|
|
context.tmp_root = tempfile.mkdtemp()
|
|
src = Path(context.tmp_root) / "src" / "cleveragents"
|
|
app_dir = src / "application"
|
|
svc_dir = app_dir / "services"
|
|
svc_dir.mkdir(parents=True)
|
|
(app_dir / "bad.py").write_text("import threading\n")
|
|
(svc_dir / "bad_svc.py").write_text("from sqlalchemy import Column\n")
|
|
context.project_root = context.tmp_root
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Whens - ADR-002
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the ADR-002 async model checker runs against that source directory")
|
|
def when_run_adr002(context):
|
|
context.adr002_result = _adr.check_adr002_async_model(context.src_dir)
|
|
_cleanup(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Whens - ADR-003
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the ADR-003 dependency injection checker runs against that source directory")
|
|
def when_run_adr003(context):
|
|
context.adr003_result = _adr.check_adr003_dependency_injection(context.src_dir)
|
|
_cleanup(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Whens - ADR-007
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the ADR-007 repository pattern checker runs against that source directory")
|
|
def when_run_adr007(context):
|
|
context.adr007_result = _adr.check_adr007_repository_pattern(context.src_dir)
|
|
_cleanup(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Whens - main()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@when("the ADR compliance main function runs with a nonexistent source directory")
|
|
def when_main_no_src(context):
|
|
context.tmp_root = tempfile.mkdtemp()
|
|
captured = StringIO()
|
|
original_cwd = os.getcwd()
|
|
try:
|
|
os.chdir(context.tmp_root)
|
|
with patch("sys.stdout", captured):
|
|
context.main_exit_code = _adr.main()
|
|
finally:
|
|
os.chdir(original_cwd)
|
|
context.main_output = captured.getvalue()
|
|
_cleanup(context)
|
|
|
|
|
|
@when("the ADR compliance main function runs against that project tree")
|
|
def when_main_project_tree(context):
|
|
captured = StringIO()
|
|
original_cwd = os.getcwd()
|
|
try:
|
|
os.chdir(context.project_root)
|
|
with patch("sys.stdout", captured):
|
|
context.main_exit_code = _adr.main()
|
|
finally:
|
|
os.chdir(original_cwd)
|
|
context.main_output = captured.getvalue()
|
|
_cleanup(context)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thens - ADR-002
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the ADR-002 checker should return an empty violations list")
|
|
def then_adr002_empty(context):
|
|
assert context.adr002_result == [], (
|
|
f"Expected no ADR-002 violations but got: {context.adr002_result}"
|
|
)
|
|
|
|
|
|
@then('the ADR-002 checker should return a violation mentioning "{fragment}"')
|
|
def then_adr002_violation_contains(context, fragment):
|
|
assert len(context.adr002_result) > 0, "Expected at least one ADR-002 violation"
|
|
matched = any(fragment in v for v in context.adr002_result)
|
|
assert matched, (
|
|
f"Expected a violation containing '{fragment}', got: {context.adr002_result}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thens - ADR-003
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the ADR-003 checker should return an empty violations list")
|
|
def then_adr003_empty(context):
|
|
assert context.adr003_result == [], (
|
|
f"Expected no ADR-003 violations but got: {context.adr003_result}"
|
|
)
|
|
|
|
|
|
@then('the ADR-003 checker should return a violation mentioning "{fragment}"')
|
|
def then_adr003_violation_contains(context, fragment):
|
|
assert len(context.adr003_result) > 0, "Expected at least one ADR-003 violation"
|
|
matched = any(fragment in v for v in context.adr003_result)
|
|
assert matched, (
|
|
f"Expected a violation containing '{fragment}', got: {context.adr003_result}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thens - ADR-007
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the ADR-007 checker should return an empty violations list")
|
|
def then_adr007_empty(context):
|
|
assert context.adr007_result == [], (
|
|
f"Expected no ADR-007 violations but got: {context.adr007_result}"
|
|
)
|
|
|
|
|
|
@then('the ADR-007 checker should return a violation mentioning "{fragment}"')
|
|
def then_adr007_violation_contains(context, fragment):
|
|
assert len(context.adr007_result) > 0, "Expected at least one ADR-007 violation"
|
|
matched = any(fragment in v for v in context.adr007_result)
|
|
assert matched, (
|
|
f"Expected a violation containing '{fragment}', got: {context.adr007_result}"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Thens - main()
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@then("the ADR compliance main should return exit code {code:d}")
|
|
def then_main_exit_code(context, code):
|
|
assert context.main_exit_code == code, (
|
|
f"Expected exit code {code} but got {context.main_exit_code}"
|
|
)
|
|
|
|
|
|
@then('the ADR compliance main output should contain "{fragment}"')
|
|
def then_main_output_contains(context, fragment):
|
|
assert fragment in context.main_output, (
|
|
f"Expected output containing '{fragment}', got:\n{context.main_output}"
|
|
)
|