Files
cleveragents-core/features/steps/validation_pipeline_steps.py
freemo 4ad51561fc perf(tests): optimize medium-slow BDD features (10-100s tier)
Cap time.sleep and asyncio.sleep globally at 10ms in before_all to
eliminate retry/backoff waits (tenacity wait_fixed, retry_auto_debug
exponential backoff). Save originals as time._original_sleep and
asyncio._original_sleep for tests that need real wall-clock delays
(CircuitBreaker recovery, debounce timers, validation timeouts).

Enhance template-DB patch: add "db." prefix to _SCENARIO_DB_PREFIXES
and check db_path.stat().st_size > 0 so 0-byte auto-created SQLite
files receive the template copy instead of falling through to real
migrations.

Replace subprocess.run CLI invocations with typer.testing.CliRunner
in module_coverage, main_coverage_complete, and coverage_extras step
files, eliminating ~6s Python cold-start overhead per call.

Switch plan_persistence and action_persistence to in-memory SQLite by
default; only cross-restart scenarios use file-based via an explicit
Given step.

Replace MigrationRunner.run_migrations with Base.metadata.create_all
in plan_service_steps for freshly created in-memory engines.

Removed leftover debug comment in environment.py.

Total tier runtime: 565s -> 21s (96% reduction), 20 features all
under 5s behave-internal time.

ISSUES CLOSED: #480
2026-03-02 02:01:27 +00:00

548 lines
20 KiB
Python

"""Step definitions for validation pipeline scenarios.
All step names are prefixed with 'validation pipeline' context to avoid
AmbiguousStep collisions with existing step definitions.
Uses the ``re`` step matcher for patterns that have optional trailing parts.
"""
from __future__ import annotations
import sys
import time
from typing import Any
from behave import given, then, use_step_matcher, when
from cleveragents.application.services.validation_pipeline import (
ValidationCommand,
ValidationPipeline,
ValidationResult,
ValidationSummary,
)
from cleveragents.domain.models.core.tool import ValidationMode
# ---------------------------------------------------------------------------
# Mock executor
# ---------------------------------------------------------------------------
class MockValidationExecutor:
"""Configurable mock executor for testing the validation pipeline."""
def __init__(self) -> None:
self._results: dict[str, dict[str, Any]] = {}
self._timeout_names: set[str] = set()
self._exception_names: dict[str, Exception] = {}
self._non_dict_names: set[str] = set()
self._missing_passed_names: set[str] = set()
self._default_passed: bool = False
self._stdout_names: set[str] = set()
self._stderr_names: set[str] = set()
def set_passed(self, name: str) -> None:
self._results[name] = {"passed": True, "message": f"{name} passed"}
def set_passed_with_data(self, name: str) -> None:
self._results[name] = {
"passed": True,
"message": f"{name} passed with data",
"data": {"detail": "some-value", "count": 42},
}
def set_failed(self, name: str, message: str) -> None:
self._results[name] = {"passed": False, "message": message}
def set_timeout(self, name: str) -> None:
self._timeout_names.add(name)
def set_exception(self, name: str, exc: Exception) -> None:
self._exception_names[name] = exc
def set_non_dict(self, name: str) -> None:
self._non_dict_names.add(name)
def set_missing_passed(self, name: str) -> None:
self._missing_passed_names.add(name)
def set_default_passed(self) -> None:
self._default_passed = True
def set_non_bool_passed(self, name: str) -> None:
self._results[name] = {"passed": "yes", "message": f"{name} passed"}
def set_non_string_message(self, name: str) -> None:
self._results[name] = {"passed": True, "message": 12345}
def set_non_dict_data(self, name: str) -> None:
self._results[name] = {"passed": True, "message": f"{name} ok", "data": [1, 2]}
def set_prints_stdout(self, name: str) -> None:
self._results[name] = {"passed": True, "message": f"{name} ok"}
self._stdout_names.add(name)
def set_prints_stderr(self, name: str) -> None:
self._results[name] = {"passed": True, "message": f"{name} ok"}
self._stderr_names.add(name)
def __call__(
self, validation_name: str, arguments: dict[str, Any]
) -> dict[str, Any]:
if validation_name in self._timeout_names:
# Sleep longer than the test timeout (0.2 s) but not excessively.
# Use _original_sleep to bypass the fast-sleep test patch.
_real_sleep = getattr(time, "_original_sleep", time.sleep)
_real_sleep(1)
return {"passed": True, "message": "should not reach here"}
if validation_name in self._exception_names:
raise self._exception_names[validation_name]
if validation_name in self._non_dict_names:
return "not-a-dict" # type: ignore[return-value]
if validation_name in self._missing_passed_names:
return {"message": "no passed key here"}
if validation_name in self._stdout_names:
print(f"stdout from {validation_name}")
if validation_name in self._stderr_names:
print(f"stderr from {validation_name}", file=sys.stderr)
if validation_name in self._results:
return self._results[validation_name]
if self._default_passed:
return {"passed": True, "message": f"{validation_name} passed (default)"}
return {"passed": False, "message": f"{validation_name}: no result configured"}
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
_RESOURCE_ID_COUNTER: int = 0
def _next_resource_id() -> str:
global _RESOURCE_ID_COUNTER
_RESOURCE_ID_COUNTER += 1
return f"R{_RESOURCE_ID_COUNTER:05d}"
# ---------------------------------------------------------------------------
# Given steps — use "re" matcher for the required/informational patterns
# ---------------------------------------------------------------------------
use_step_matcher("re")
@given(r"a validation pipeline test environment")
def step_given_pipeline_env(context: Any) -> None:
context.vp_commands: list[ValidationCommand] = []
context.vp_executor = MockValidationExecutor()
context.vp_max_workers: int = 4
context.vp_read_only_resources: set[str] = set()
context.vp_summary: ValidationSummary | None = None
context.vp_plan_metadata: dict[str, Any] | None = None
context.vp_resource_id_map: dict[str, str] = {}
global _RESOURCE_ID_COUNTER
_RESOURCE_ID_COUNTER = 0
@given(r"an empty list of validation commands")
def step_given_empty_commands(context: Any) -> None:
context.vp_commands = []
@given(
r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"'
r" with timeout (?P<timeout>[\d.]+)"
)
def step_given_required_command_timeout(
context: Any, name: str, resource: str, timeout: str
) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
timeout_seconds=float(timeout),
)
)
@given(
r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"'
r' having resource_id "(?P<rid>[^"]+)"'
)
def step_given_required_command_with_rid(
context: Any, name: str, resource: str, rid: str
) -> None:
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
)
)
@given(r'a required vp command "(?P<name>[^"]+)" on resource "(?P<resource>[^"]+)"')
def step_given_required_command(context: Any, name: str, resource: str) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.REQUIRED,
arguments={},
)
)
@given(
r'an informational vp command "(?P<name>[^"]+)"'
r' on resource "(?P<resource>[^"]+)"'
)
def step_given_informational_command(context: Any, name: str, resource: str) -> None:
rid = context.vp_resource_id_map.get(resource, _next_resource_id())
context.vp_resource_id_map[resource] = rid
context.vp_commands.append(
ValidationCommand(
validation_name=name,
resource_id=rid,
resource_name=resource,
mode=ValidationMode.INFORMATIONAL,
arguments={},
)
)
@given(r'the vp executor returns passed for "(?P<name>[^"]+)"')
def step_given_executor_passed(context: Any, name: str) -> None:
context.vp_executor.set_passed(name)
@given(
r'the vp executor returns failed for "(?P<name>[^"]+)"'
r' with message "(?P<message>[^"]+)"'
)
def step_given_executor_failed(context: Any, name: str, message: str) -> None:
context.vp_executor.set_failed(name, message)
@given(r"the vp executor returns passed for all validations")
def step_given_executor_all_passed(context: Any) -> None:
context.vp_executor.set_default_passed()
@given(r'the vp executor will timeout for "(?P<name>[^"]+)"')
def step_given_executor_timeout(context: Any, name: str) -> None:
context.vp_executor.set_timeout(name)
@given(r'the vp executor returns non-dict output for "(?P<name>[^"]+)"')
def step_given_executor_non_dict(context: Any, name: str) -> None:
context.vp_executor.set_non_dict(name)
@given(r'the vp executor returns dict without passed key for "(?P<name>[^"]+)"')
def step_given_executor_missing_passed(context: Any, name: str) -> None:
context.vp_executor.set_missing_passed(name)
@given(r'the vp executor raises an exception for "(?P<name>[^"]+)"')
def step_given_executor_exception(context: Any, name: str) -> None:
context.vp_executor.set_exception(name, RuntimeError("Test exception"))
@given(r'the vp executor returns passed with data for "(?P<name>[^"]+)"')
def step_given_executor_passed_with_data(context: Any, name: str) -> None:
context.vp_executor.set_passed_with_data(name)
@given(r'the vp resource "(?P<rid>[^"]+)" is marked as read-only')
def step_given_read_only_resource(context: Any, rid: str) -> None:
context.vp_read_only_resources.add(rid)
@given(r"a validation summary with (?P<count>\d+) required failures")
def step_given_summary_with_failures(context: Any, count: str) -> None:
cnt = int(count)
context.vp_summary = ValidationSummary(
total=cnt + 1,
required_passed=1,
required_failed=cnt,
informational_passed=0,
informational_failed=0,
results=[],
)
@given(r"the pipeline max_workers is set to (?P<workers>\d+)")
def step_given_max_workers(context: Any, workers: str) -> None:
context.vp_max_workers = int(workers)
# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------
@when(r"the validation pipeline runs for plan with metadata")
def step_when_pipeline_runs_for_plan(context: Any) -> None:
context.vp_plan_metadata = {}
pipeline = ValidationPipeline(
commands=context.vp_commands,
executor=context.vp_executor,
max_workers=context.vp_max_workers,
read_only_resources=context.vp_read_only_resources,
)
context.vp_summary = pipeline.run_for_plan(plan_metadata=context.vp_plan_metadata)
@when(r"the validation pipeline runs")
def step_when_pipeline_runs(context: Any) -> None:
pipeline = ValidationPipeline(
commands=context.vp_commands,
executor=context.vp_executor,
max_workers=context.vp_max_workers,
read_only_resources=context.vp_read_only_resources,
)
context.vp_summary = pipeline.run()
# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------
@then(r"the validation summary total is (?P<count>\d+)")
def step_then_summary_total(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None, "No summary available"
assert context.vp_summary.total == cnt, (
f"Expected total={cnt}, got {context.vp_summary.total}"
)
@then(r"the validation summary required_passed is (?P<count>\d+)")
def step_then_summary_required_passed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.required_passed == cnt, (
f"Expected required_passed={cnt}, got {context.vp_summary.required_passed}"
)
@then(r"the validation summary required_failed is (?P<count>\d+)")
def step_then_summary_required_failed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.required_failed == cnt, (
f"Expected required_failed={cnt}, got {context.vp_summary.required_failed}"
)
@then(r"the validation summary informational_passed is (?P<count>\d+)")
def step_then_summary_informational_passed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.informational_passed == cnt, (
f"Expected informational_passed={cnt}, "
f"got {context.vp_summary.informational_passed}"
)
@then(r"the validation summary informational_failed is (?P<count>\d+)")
def step_then_summary_informational_failed(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
assert context.vp_summary.informational_failed == cnt, (
f"Expected informational_failed={cnt}, "
f"got {context.vp_summary.informational_failed}"
)
@then(r"all required validations passed")
def step_then_all_required_passed(context: Any) -> None:
assert context.vp_summary is not None
assert context.vp_summary.all_required_passed, (
f"Expected all_required_passed=True, got False "
f"(required_failed={context.vp_summary.required_failed})"
)
@then(r"not all required validations passed")
def step_then_not_all_required_passed(context: Any) -> None:
assert context.vp_summary is not None
assert not context.vp_summary.all_required_passed, (
"Expected all_required_passed=False, got True"
)
@then(r"the validation results are sorted by resource then mode then name")
def step_then_results_sorted(context: Any) -> None:
assert context.vp_summary is not None
results = context.vp_summary.results
keys = [(r.resource_name, r.mode.value, r.validation_name) for r in results]
assert keys == sorted(keys), f"Results not sorted: {keys}"
@then(r'the vp result for "(?P<name>[^"]+)" has timed_out true')
def step_then_result_timed_out(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.timed_out, f"Expected timed_out=True for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has passed false')
def step_then_result_passed_false(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert not result.passed, f"Expected passed=False for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has passed true')
def step_then_result_passed_true(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.passed, f"Expected passed=True for '{name}'"
@then(r'the vp result for "(?P<name>[^"]+)" has error containing "(?P<text>[^"]+)"')
def step_then_result_error_contains(context: Any, name: str, text: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.error is not None, f"Expected error for '{name}', got None"
assert text in result.error, (
f"Expected error containing '{text}', got: {result.error}"
)
@then(r'the vp result for "(?P<name>[^"]+)" has message containing "(?P<text>[^"]+)"')
def step_then_result_message_contains(context: Any, name: str, text: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert text in result.message, (
f"Expected message containing '{text}', got: {result.message}"
)
@then(r'the vp result for "(?P<name>[^"]+)" has data key "(?P<key>[^"]+)"')
def step_then_result_has_data_key(context: Any, name: str, key: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.data is not None, f"Expected data for '{name}', got None"
assert key in result.data, (
f"Expected data key '{key}' in {list(result.data.keys())}"
)
@then(r"the results grouped by resource have (?P<count>\d+) groups")
def step_then_groups_count(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
groups = ValidationPipeline.group_by_resource(context.vp_summary.results)
assert len(groups) == cnt, f"Expected {cnt} groups, got {len(groups)}"
@then(r'the vp resource group "(?P<resource>[^"]+)" has (?P<count>\d+) result')
def step_then_group_result_count(context: Any, resource: str, count: str) -> None:
cnt = int(count)
assert context.vp_summary is not None
groups = ValidationPipeline.group_by_resource(context.vp_summary.results)
assert resource in groups, f"Resource '{resource}' not in groups"
actual = len(groups[resource])
assert actual == cnt, f"Expected {cnt} results for '{resource}', got {actual}"
@then(r"the summary all_required_passed property is true")
def step_then_summary_property_true(context: Any) -> None:
assert context.vp_summary is not None
assert context.vp_summary.all_required_passed
@then(r"the summary all_required_passed property is false")
def step_then_summary_property_false(context: Any) -> None:
assert context.vp_summary is not None
assert not context.vp_summary.all_required_passed
@then(r"the plan metadata contains validation_summary")
def step_then_plan_metadata_has_summary(context: Any) -> None:
assert context.vp_plan_metadata is not None
assert "validation_summary" in context.vp_plan_metadata
@then(r"the plan metadata validation_summary total is (?P<count>\d+)")
def step_then_plan_metadata_total(context: Any, count: str) -> None:
cnt = int(count)
assert context.vp_plan_metadata is not None
vs = context.vp_plan_metadata["validation_summary"]
assert vs["total"] == cnt, f"Expected total={cnt}, got {vs['total']}"
@given(r'the vp executor returns non-bool passed for "(?P<name>[^"]+)"')
def step_given_executor_non_bool_passed(context: Any, name: str) -> None:
context.vp_executor.set_non_bool_passed(name)
@given(r'the vp executor returns non-string message for "(?P<name>[^"]+)"')
def step_given_executor_non_string_message(context: Any, name: str) -> None:
context.vp_executor.set_non_string_message(name)
@given(r'the vp executor returns non-dict data for "(?P<name>[^"]+)"')
def step_given_executor_non_dict_data(context: Any, name: str) -> None:
context.vp_executor.set_non_dict_data(name)
@given(r'the vp executor prints to stdout for "(?P<name>[^"]+)"')
def step_given_executor_prints_stdout(context: Any, name: str) -> None:
context.vp_executor.set_prints_stdout(name)
@given(r'the vp executor prints to stderr for "(?P<name>[^"]+)"')
def step_given_executor_prints_stderr(context: Any, name: str) -> None:
context.vp_executor.set_prints_stderr(name)
@then(r'the vp result for "(?P<name>[^"]+)" has positive duration_ms')
def step_then_result_positive_duration(context: Any, name: str) -> None:
assert context.vp_summary is not None
result = _find_result(context.vp_summary, name)
assert result.duration_ms > 0, (
f"Expected duration_ms > 0 for '{name}', got {result.duration_ms}"
)
# Reset to default parse matcher
use_step_matcher("parse")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _find_result(summary: ValidationSummary, name: str) -> ValidationResult:
"""Find a validation result by name."""
for r in summary.results:
if r.validation_name == name:
return r
available = [r.validation_name for r in summary.results]
msg = f"No result for '{name}', available: {available}"
raise AssertionError(msg)