80f1664a0d
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 3m20s
CI / typecheck (pull_request) Successful in 3m54s
CI / security (pull_request) Successful in 4m13s
CI / quality (pull_request) Successful in 4m17s
CI / integration_tests (pull_request) Successful in 9m21s
CI / unit_tests (pull_request) Successful in 9m42s
CI / docker (pull_request) Successful in 14s
CI / e2e_tests (pull_request) Successful in 12m1s
CI / coverage (pull_request) Successful in 11m41s
CI / status-check (pull_request) Successful in 1s
CI / build (push) Successful in 26s
CI / lint (push) Successful in 3m19s
CI / quality (push) Successful in 3m41s
CI / typecheck (push) Successful in 3m56s
CI / benchmark-regression (push) Has been skipped
CI / security (push) Successful in 4m2s
CI / integration_tests (push) Successful in 9m3s
CI / unit_tests (push) Successful in 9m24s
CI / docker (push) Successful in 1m9s
CI / e2e_tests (push) Successful in 13m51s
CI / coverage (push) Successful in 11m25s
CI / status-check (push) Successful in 1s
CI / benchmark-publish (push) Failing after 24m52s
CI / benchmark-regression (pull_request) Failing after 37m6s
Implemented Robot Framework integration test suite validating the CI/CD workflow (Specification Example 7). Tests cover ci-profile configuration (automation-profile, format, log level), idempotent resource and project registration with duplicate-detection assertions, three validation tools (ci-lint, ci-typecheck, ci-tests) registration and resource attachment via ToolRegistryService, action creation with typed arguments and invariants per spec Step 2, plan lifecycle with phase-by-phase completion through all phases (strategize, execute, apply) until terminal applied state, and JSON output structure verification including plan_id, phase, state, action, projects, and arguments fields. Post-review fixes applied (round 1): - H1: Fix truncated invariant #2 text to match spec line 39051 - H2: Fix definition_of_done to match spec's 5-bullet-point format - M1: Register all 3 spec validations (ci-lint, ci-typecheck, ci-tests) - M3: Add branch property to resource registration per spec Step 3 - M4: Replace phantom resource_id with real registered resource - M5: Add projects and arguments field assertions to JSON output test - M7: Replace 'polling-based' with 'phase-by-phase' in docs/comments - L1: Use timezone-aware datetime (timezone.utc) - L2: Use tempfile for resource path instead of hardcoded /tmp - L3: Clarify json_output() docstring to reflect as_cli_dict() scope - L4: Tighten attachment count assertion from >= 1 to == 1 (now == 3) Post-review fixes applied (round 2): - M2: Use Setup Test Environment With Database Isolation for pabot safety - L1: Replace remaining hardcoded /tmp paths with tempfile (validation_attach, resource_idempotent duplicate registration) - L2: Fix stale 'Polling-based' comment missed in round 1 - L5: Align action description with spec line 39027 - M1/L3/L6: Document automation_profile propagation gap in use_action() with TODO for when production code wires the field onto Plan - L4: Add comment explaining local actor name deviation from spec Includes CHANGELOG update describing the integration test scope. ISSUES CLOSED: #771
163 lines
5.7 KiB
Python
163 lines
5.7 KiB
Python
"""ASV benchmarks for session list service-layer performance baseline (bug #554).
|
|
|
|
Measures the cost of listing sessions through ``PersistentSessionService``
|
|
using a file-based SQLite database. These benchmarks construct the service
|
|
directly (bypassing DI wiring) and therefore do **not** exercise the
|
|
``_get_session_service()`` code path that triggers bug #554. Their purpose
|
|
is to establish a performance baseline for the service layer itself.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
_SRC = str(Path(__file__).resolve().parents[1] / "src")
|
|
if _SRC not in sys.path:
|
|
sys.path.insert(0, _SRC)
|
|
|
|
from sqlalchemy import create_engine # noqa: E402
|
|
from sqlalchemy.orm import sessionmaker # noqa: E402
|
|
|
|
from cleveragents.application.services.session_service import ( # noqa: E402
|
|
PersistentSessionService,
|
|
)
|
|
from cleveragents.infrastructure.database.models import Base # noqa: E402
|
|
from cleveragents.infrastructure.database.repositories import ( # noqa: E402
|
|
SessionMessageRepository,
|
|
SessionRepository,
|
|
)
|
|
|
|
|
|
class SessionListDISuite:
|
|
"""Benchmark session list through the service layer (direct construction).
|
|
|
|
Engine and sessionmaker are built once in ``setup()`` and reused across
|
|
``time_list_empty`` iterations. Methods that mutate state
|
|
(``time_list_after_create``, ``track_list_after_create_count``) use
|
|
per-method setup/teardown to get a fresh database each time, preventing
|
|
row accumulation across ASV iterations.
|
|
|
|
Does **not** exercise ``_get_session_service()`` or the DI container
|
|
wiring.
|
|
"""
|
|
|
|
timeout = 30.0
|
|
|
|
# -- suite-level setup (shared engine for read-only benchmarks) ----------
|
|
|
|
def setup(self) -> None:
|
|
self._tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_")
|
|
self._db_path = os.path.join(self._tmpdir, "bench.db")
|
|
self._engine = create_engine(
|
|
f"sqlite:///{self._db_path}",
|
|
echo=False,
|
|
)
|
|
Base.metadata.create_all(self._engine)
|
|
self._session_factory = sessionmaker(
|
|
bind=self._engine,
|
|
expire_on_commit=False,
|
|
)
|
|
# Pre-initialise attributes used by per-method setup hooks so that
|
|
# benchmarks still work even when the ASV runner skips per-method
|
|
# setup_<name>() callbacks (e.g. in fork-server mode).
|
|
self._empty_svc = self._make_service()
|
|
self._fresh_engine()
|
|
|
|
def teardown(self) -> None:
|
|
self._engine.dispose()
|
|
shutil.rmtree(self._tmpdir, ignore_errors=True)
|
|
|
|
def _make_service(self) -> PersistentSessionService:
|
|
"""Build a PersistentSessionService using the shared session factory."""
|
|
return PersistentSessionService(
|
|
session_repo=SessionRepository(
|
|
session_factory=self._session_factory,
|
|
),
|
|
message_repo=SessionMessageRepository(
|
|
session_factory=self._session_factory,
|
|
),
|
|
)
|
|
|
|
# -- per-method setup for mutating benchmarks ----------------------------
|
|
|
|
def _fresh_engine(self) -> None:
|
|
"""Create an isolated engine+factory so create() doesn't accumulate."""
|
|
self._mut_tmpdir = tempfile.mkdtemp(prefix="bench_sle_554_mut_")
|
|
db_path = os.path.join(self._mut_tmpdir, "bench.db")
|
|
self._mut_engine = create_engine(
|
|
f"sqlite:///{db_path}",
|
|
echo=False,
|
|
)
|
|
Base.metadata.create_all(self._mut_engine)
|
|
self._mut_factory = sessionmaker(
|
|
bind=self._mut_engine,
|
|
expire_on_commit=False,
|
|
)
|
|
|
|
def _dispose_fresh(self) -> None:
|
|
if hasattr(self, "_mut_engine"):
|
|
self._mut_engine.dispose()
|
|
if hasattr(self, "_mut_tmpdir"):
|
|
shutil.rmtree(self._mut_tmpdir, ignore_errors=True)
|
|
|
|
def _make_mut_service(self) -> PersistentSessionService:
|
|
return PersistentSessionService(
|
|
session_repo=SessionRepository(session_factory=self._mut_factory),
|
|
message_repo=SessionMessageRepository(
|
|
session_factory=self._mut_factory,
|
|
),
|
|
)
|
|
|
|
# -- ASV per-method hooks ------------------------------------------------
|
|
|
|
def setup_time_list_after_create(self) -> None:
|
|
self._fresh_engine()
|
|
|
|
def teardown_time_list_after_create(self) -> None:
|
|
self._dispose_fresh()
|
|
|
|
def setup_track_list_after_create_count(self) -> None:
|
|
self._fresh_engine()
|
|
|
|
def teardown_track_list_after_create_count(self) -> None:
|
|
self._dispose_fresh()
|
|
|
|
def setup_time_list_empty(self) -> None:
|
|
self._empty_svc = self._make_service()
|
|
|
|
# -- benchmarks ----------------------------------------------------------
|
|
|
|
def time_list_empty(self) -> None:
|
|
"""List sessions when DB is empty (service-layer only, no DI)."""
|
|
self._empty_svc.list()
|
|
|
|
def time_list_after_create(self) -> None:
|
|
"""Create then list (service-layer only, fresh DB per iteration).
|
|
|
|
Bypasses ``_get_session_service()`` / DI container — measures the
|
|
service-layer round-trip cost, not the DI wiring path.
|
|
"""
|
|
svc = self._make_mut_service()
|
|
svc.create()
|
|
svc.list()
|
|
|
|
def track_list_after_create_count(self) -> int:
|
|
"""Track session list persistence at the service layer.
|
|
|
|
Returns the count of sessions visible after a create. Uses a
|
|
fresh DB per iteration so the count is always exactly 1.
|
|
This benchmark constructs ``PersistentSessionService`` directly,
|
|
bypassing the DI container — it does **not** reproduce bug #554.
|
|
"""
|
|
svc = self._make_mut_service()
|
|
svc.create(actor_name="bench/test")
|
|
sessions = svc.list()
|
|
return len(sessions)
|
|
|
|
|
|
SessionListDISuite.track_list_after_create_count.unit = "sessions"
|