Source code for behave_kit.timing
"""`assert_under` and `@timed` — time-based assertions for performance checks.
`assert_under` verifies that a callable completes within a deadline.
`@timed` is a decorator that asserts a step finishes within a time limit.
"""
from __future__ import annotations
import time
from collections.abc import Callable
from functools import wraps
from typing import Concatenate, ParamSpec, TypeVar, cast
from behave_kit._core.errors import BehaveKitError
from behave_kit._core.types import Context
P = ParamSpec("P")
R = TypeVar("R")
[docs]
class TimeoutExceededError(BehaveKitError):
"""Raised when a callable takes longer than the allowed time."""
[docs]
def assert_under(
seconds: float,
func: Callable[[], R],
*,
message: str = "",
) -> R:
"""Run ``func`` and assert it completes within ``seconds``.
Args:
seconds: Maximum allowed execution time.
func: Zero-argument callable to time.
message: Custom message for the timeout error.
Returns:
The return value of ``func``.
Raises:
TimeoutExceededError: If ``func`` takes longer than ``seconds``.
"""
if seconds < 0:
raise BehaveKitError(
f"seconds must be non-negative, got {seconds}",
suggestion="Use a positive float for the time limit",
)
start = time.monotonic()
result = func()
elapsed = time.monotonic() - start
if elapsed > seconds:
raise TimeoutExceededError(
message or f"Execution took {elapsed:.3f}s, expected under {seconds}s",
suggestion="Optimize the operation or increase the time limit",
)
return result
[docs]
def timed(
seconds: float,
) -> Callable[
[Callable[Concatenate[Context, P], R]],
Callable[Concatenate[Context, P], R],
]:
"""Decorator: assert a step function completes within ``seconds``.
Usage::
@timed(2.0)
@when("I fetch the data")
def step(context): ...
Raises:
BehaveKitError: If ``seconds`` is negative.
"""
if seconds < 0:
raise BehaveKitError(
f"seconds must be non-negative, got {seconds}",
suggestion="Use a positive float for the time limit",
)
def decorator(
func: Callable[Concatenate[Context, P], R],
) -> Callable[Concatenate[Context, P], R]:
"""Wrap ``func`` to assert it finishes within the time limit."""
@wraps(func)
def wrapper(context: Context, *args: P.args, **kwargs: P.kwargs) -> R:
"""Time the step and raise if it exceeds the limit."""
start = time.monotonic()
result = func(context, *args, **kwargs)
elapsed = time.monotonic() - start
if elapsed > seconds:
raise TimeoutExceededError(
f"Step '{func.__name__}' took {elapsed:.3f}s, expected under {seconds}s",
suggestion="Optimize the step or increase the time limit",
)
return result
return cast("Callable[Concatenate[Context, P], R]", wrapper)
return decorator