GitHub

API reference for testing.

Deterministic tests, and a pytest plugin to write them as effects.

A TestClock only moves when the test says so, with adjust or set_time; each move lets ready fibers run, then wakes sleepers in order and waits for each woken fiber to suspend, so timing tests are exact and take no real time::

@testing.effect
def test_timeout():
    fiber = yield from fx.sleep(10).timeout(5).exit().fork()
    yield from testing.adjust(5)
    assert isinstance((yield from fiber.join()), fx.Failure)
  • effect / live: pytest tests written as generators or effects, with a TestClock and TestConsole (or the live ones) and a Scope, a real-time timeout that interrupts them and runs their finalizers, and asyncio=True for effects that need an event loop.
  • layer_fixture: build a layer once per pytest scope; tests that take the fixture as Services[...] get its services.
  • effect_fixture: run a scoped effect once per pytest scope; tests get its value, and its finalizers run when the scope ends.
  • flaky: retry a test effect that sometimes fails.
  • run_test / run_test_async: run one effect with a TestClock, synchronously or on the running asyncio loop.
  • with_live, TestClock.layer(): the live clock inside a test, and a test clock inside any program.
  • TestConsole: a console recording every call; effect tests run with a fresh one, read back with log_lines / error_lines.
  • assert_exit_success and friends: failures print Cause.pretty().

If every fiber ends up waiting with nothing to wake them, synchronous runs raise StalledError instead of hanging. A woken fiber that never suspends makes adjust warn with BusyFiberWarning and move on.

Classes

Functions

assert_exit_failure

assert_exit_failure(ex, expected) -> None

ex is a Failure whose cause equals expected.

ParameterTypeDescription
exExit[Any, E]
expectedCause[E]
def assert_exit_failure[E](ex: Exit[Any, E], expected: Cause[E]) -> None:
    """``ex`` is a ``Failure`` whose cause equals ``expected``."""
    if isinstance(ex, Failure) and ex.cause == expected:
        return
    raise AssertionError(
        f"expected Failure with cause:\n{_pretty(expected)}\ngot {describe_exit(ex)}"
    )

assert_exit_success

assert_exit_success(ex, expected) -> None

ex is Success(expected).

ParameterTypeDescription
exExit[A, Any]
expectedA
def assert_exit_success[A](ex: Exit[A, Any], expected: A) -> None:
    """``ex`` is ``Success(expected)``."""
    if isinstance(ex, Failure):
        raise AssertionError(f"expected Success({expected!r}), got {describe_exit(ex)}")
    if ex.value != expected:
        raise AssertionError(f"expected Success({expected!r}), got Success({ex.value!r})")

assert_failure

assert_failure(result, expected) -> None

result is Err(expected).

ParameterTypeDescription
resultResult[Any, E]
expectedE
def assert_failure[E](result: Result[Any, E], expected: E) -> None:
    """``result`` is ``Err(expected)``."""
    if result != Err(expected):
        raise AssertionError(f"expected Err({expected!r}), got {result!r}")

assert_success

assert_success(result, expected) -> None

result is Ok(expected).

ParameterTypeDescription
resultResult[A, Any]
expectedA
def assert_success[A](result: Result[A, Any], expected: A) -> None:
    """``result`` is ``Ok(expected)``."""
    if result != Ok(expected):
        raise AssertionError(f"expected Ok({expected!r}), got {result!r}")

adjust

adjust(duration) -> Effect[None]

Move the test clock forward by duration.

ParameterTypeDescription
durationDuration

Returns Effect[None]

def adjust(duration: Duration) -> Effect[None]:
    """Move the test clock forward by ``duration``."""
    return test_clock().flat_map(lambda c: c.adjust(duration))

set_time

set_time(at) -> Effect[None]

Set the test clock's wall time to at (seconds since the epoch).

ParameterTypeDescription
atfloat

Returns Effect[None]

def set_time(at: float) -> Effect[None]:
    """Set the test clock's wall time to ``at`` (seconds since the epoch)."""
    return test_clock().flat_map(lambda c: c.set_time(at))

test_clock

test_clock() -> Effect[TestClock]

The TestClock the effect runs with; dies if the clock is live.

Returns Effect[TestClock]

def test_clock() -> Effect[TestClock]:
    """The ``TestClock`` the effect runs with; dies if the clock is live."""

    def check(clock: ClockService) -> Effect[TestClock]:
        if not isinstance(clock, TestClock):
            raise TypeError(
                "no TestClock in context; run the effect with a pyfect.testing runner "
                "or provide TestClock.layer()"
            )
        return _Succeed(clock)

    return Clock().flat_map(check)

with_live

with_live(eff) -> Effect[A, E, R]

Run eff with the live clock. Under a TestClock that is the clock it replaced; otherwise the clock is already live.

ParameterTypeDescription
effEffect[A, E, R]

Returns Effect[A, E, R]

def with_live[A, E, R](eff: Effect[A, E, R]) -> Effect[A, E, R]:
    """Run ``eff`` with the live clock. Under a ``TestClock`` that is the
    clock it replaced; otherwise the clock is already live."""
    require_effect(eff, "eff")

    def run(clock: ClockService) -> Effect[A, E, R]:
        return clock.with_live(eff) if isinstance(clock, TestClock) else eff

    return Clock().flat_map(run)

test_console

test_console() -> Effect[TestConsole]

The TestConsole the effect runs with; dies if another console is current.

Returns Effect[TestConsole]

def test_console() -> Effect[TestConsole]:
    """The ``TestConsole`` the effect runs with; dies if another console
    is current."""
    return CurrentConsole().flat_map(_check)

effect

effect(f=None, /, *, timeout=None, asyncio=False, warning_delay=1.0) -> Any

A pytest test written as an effect, run with a TestClock, a TestConsole and a Scope (closed when the test ends)::

@effect
def test_retries():
    fiber = yield from flaky_call.retry(spaced(1)).fork()
    yield from adjust(3)
    assert (yield from fiber.join()) == "ok"

timeout (real time; default from the pyfect_timeout ini option) interrupts the test and runs its finalizers. asyncio runs it on an event loop, for from_async. A first parameter annotated Services[...] (a layer_fixture; in a test class, the parameter after self) provides those services.

ParameterTypeDescription
fCallable[..., Any] | None = None
timeoutDuration | None = None
asynciobool = False
warning_delayDuration = 1.0

Returns Any

def effect(
    f: Callable[..., Any] | None = None,
    /,
    *,
    timeout: Duration | None = None,
    asyncio: bool = False,
    warning_delay: Duration = 1.0,
) -> Any:
    """A pytest test written as an effect, run with a ``TestClock``, a
    ``TestConsole`` and a ``Scope`` (closed when the test ends)::

        @effect
        def test_retries():
            fiber = yield from flaky_call.retry(spaced(1)).fork()
            yield from adjust(3)
            assert (yield from fiber.join()) == "ok"

    ``timeout`` (real time; default from the ``pyfect_timeout`` ini option)
    interrupts the test and runs its finalizers. ``asyncio`` runs it on an
    event loop, for ``from_async``. A first parameter annotated
    ``Services[...]`` (a ``layer_fixture``; in a test class, the parameter
    after ``self``) provides those services."""
    decorator = TestDecorator(_Options("effect", timeout, asyncio, warning_delay))
    return decorator if f is None else decorator(f)

live

live(f=None, /, *, timeout=None, asyncio=False) -> Any

Like effect, but with the live clock and console: sleeps take real time and output is written.

ParameterTypeDescription
fCallable[..., Any] | None = None
timeoutDuration | None = None
asynciobool = False

Returns Any

def live(
    f: Callable[..., Any] | None = None,
    /,
    *,
    timeout: Duration | None = None,
    asyncio: bool = False,
) -> Any:
    """Like ``effect``, but with the live clock and console: sleeps take
    real time and output is written."""
    decorator = TestDecorator(_Options("live", timeout, asyncio, 1.0))
    return decorator if f is None else decorator(f)

flaky

flaky(eff, timeout=30.0) -> Effect[Any, Any, Any]

Retry eff (in a fresh scope each time) after any failure, defects included, up to 10 times while less than timeout has passed. Like Effect's flakyTest, the last failure dies: its typed errors become defects, kept with their tracebacks.

ParameterTypeDescription
effEffect[Any, Any, Any]
timeoutDuration = 30.0

Returns Effect[Any, Any, Any]

def flaky(eff: Effect[Any, Any, Any], timeout: Duration = 30.0) -> Effect[Any, Any, Any]:
    """Retry ``eff`` (in a fresh scope each time) after any failure, defects
    included, up to 10 times while less than ``timeout`` has passed. Like
    Effect's ``flakyTest``, the last failure dies: its typed errors become
    defects, kept with their tracebacks."""
    require_effect(eff, "eff")
    policy = recurs(10).up_to(duration=timeout)
    attempts: Effect[Any, Any, Any] = scoped(eff).sandbox().retry(policy)
    return attempts.catch_all(lambda cause: fail_cause(_all_defects(cause)))

effect_fixture

effect_fixture(eff, *, scope='function', live=False, asyncio=False, warning_delay=1.0, timeout=None) -> FixtureFunctionDefinition

A pytest fixture whose value is what eff succeeds with, run once per scope. Finalizers eff adds to its Scope run when the scope ends; a failure is raised as the fixture's error.

The options are layer_fixture's. The value is a plain Python value, so any test can take it; to share services or forked fibers with effect tests, use layer_fixture.

ParameterTypeDescription
effEffect[A, E, Scope]
scopeFixtureScope = 'function'
livebool = False
asynciobool = False
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns FixtureFunctionDefinition

def effect_fixture[A, E](
    eff: Effect[A, E, Scope],
    *,
    scope: FixtureScope = "function",
    live: bool = False,
    asyncio: bool = False,
    warning_delay: Duration = 1.0,
    timeout: Duration | None = None,
) -> FixtureFunctionDefinition:
    """A pytest fixture whose value is what ``eff`` succeeds with, run once
    per ``scope``. Finalizers ``eff`` adds to its ``Scope`` run when the
    scope ends; a failure is raised as the fixture's error.

    The options are ``layer_fixture``'s. The value is a plain Python value,
    so any test can take it; to share services or forked fibers with
    ``effect`` tests, use ``layer_fixture``."""
    require_effect(eff, "eff")

    def fixture() -> Generator[A]:
        with _held(
            lambda resources: eff.pipe(provide_service(Scope, resources)),
            live=live,
            asyncio=asyncio,
            warning_delay=warning_delay,
            timeout=timeout,
        ) as (value, _context, _runtime):
            yield value

    return _fixture(fixture, scope)

layer_fixture

layer_fixture(layer, *, scope='module', live=False, asyncio=False, warning_delay=1.0, timeout=None) -> FixtureFunctionDefinition

A pytest fixture that builds layer once per scope and releases its resources when the scope ends.

The layer is built with a TestClock and a TestConsole (shared by every test that uses the fixture), unless live: then it and its effect tests use the live clock and console. Tests using the fixture run on the fixture's runtime, so fibers the layer forks keep running; asyncio picks an event loop for it, needed by from_async. timeout bounds building and releasing, in real time.

ParameterTypeDescription
layerLayer[RO, E]
scopeFixtureScope = 'module'
livebool = False
asynciobool = False
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns FixtureFunctionDefinition

def layer_fixture[RO, E](
    layer: Layer[RO, E],
    *,
    scope: FixtureScope = "module",
    live: bool = False,
    asyncio: bool = False,
    warning_delay: Duration = 1.0,
    timeout: Duration | None = None,
) -> FixtureFunctionDefinition:
    """A pytest fixture that builds ``layer`` once per ``scope`` and
    releases its resources when the scope ends.

    The layer is built with a ``TestClock`` and a ``TestConsole`` (shared
    by every test that uses the fixture), unless ``live``: then it and its
    ``effect`` tests use the live clock and console. Tests using the fixture run on the
    fixture's runtime, so fibers the layer forks keep running; ``asyncio``
    picks an event loop for it, needed by ``from_async``. ``timeout``
    bounds building and releasing, in real time."""
    require_layer(layer, "layer")

    def fixture() -> Generator[Services[RO]]:
        with _held(
            layer.build_with_scope,
            live=live,
            asyncio=asyncio,
            warning_delay=warning_delay,
            timeout=timeout,
        ) as (built, context, runtime):
            yield Services(context.merge(built), runtime)

    return _fixture(fixture, scope)

run_test

run_test(eff, *, stall_timeout=1.0, warning_delay=1.0, timeout=None) -> A
ParameterTypeDescription
effEffect[A, Never, Never]
stall_timeoutfloat = 1.0
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns A

def run_test[A](
    eff: Effect[A, Never, Never],
    *,
    stall_timeout: float = 1.0,
    warning_delay: Duration = 1.0,
    timeout: Duration | None = None,
) -> A:
    return unwrap(
        run_test_exit(
            eff, stall_timeout=stall_timeout, warning_delay=warning_delay, timeout=timeout
        )
    )

run_test_async

run_test_async(eff, *, warning_delay=1.0, timeout=None) -> A
ParameterTypeDescription
effEffect[A, Never, Never]
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns A

async def run_test_async[A](
    eff: Effect[A, Never, Never], *, warning_delay: Duration = 1.0, timeout: Duration | None = None
) -> A:
    return unwrap(await run_test_async_exit(eff, warning_delay=warning_delay, timeout=timeout))

run_test_async_exit

run_test_async_exit(eff, *, warning_delay=1.0, timeout=None) -> Exit[A, E]

Run eff on the running asyncio loop with a TestClock and a TestConsole, so from_async works on virtual time. Cancelling the awaiting task interrupts the effect and waits for its finalizers.

ParameterTypeDescription
effEffect[A, E, Never]
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns Exit[A, E]

async def run_test_async_exit[A, E](
    eff: Effect[A, E, Never], *, warning_delay: Duration = 1.0, timeout: Duration | None = None
) -> Exit[A, E]:
    """Run ``eff`` on the running asyncio loop with a ``TestClock`` and a
    ``TestConsole``, so
    ``from_async`` works on virtual time. Cancelling the awaiting task
    interrupts the effect and waits for its finalizers."""
    require_effect(eff, "eff")
    clock = TestClock(warning_delay=warning_delay)
    limit = None if timeout is None else to_seconds(timeout)
    try:
        return await run_on_loop(
            AsyncioScheduler.current(),
            eff,
            Context.empty().add(Clock, clock).add(CurrentConsole, TestConsole()),
            limit,
        )
    finally:
        clock.stop_warning()

run_test_exit

run_test_exit(eff, *, stall_timeout=1.0, warning_delay=1.0, timeout=None) -> Exit[A, E]

Run eff on a synchronous scheduler with a TestClock and a TestConsole.

Raises StalledError when every fiber waits with nothing to wake it, and TestTimeoutError after timeout of real time; either way the effect is interrupted and its finalizers run first.

ParameterTypeDescription
effEffect[A, E, Never]
stall_timeoutfloat = 1.0
warning_delayDuration = 1.0
timeoutDuration | None = None

Returns Exit[A, E]

def run_test_exit[A, E](
    eff: Effect[A, E, Never],
    *,
    stall_timeout: float = 1.0,
    warning_delay: Duration = 1.0,
    timeout: Duration | None = None,
) -> Exit[A, E]:
    """Run ``eff`` on a synchronous scheduler with a ``TestClock`` and a
    ``TestConsole``.

    Raises ``StalledError`` when every fiber waits with nothing to wake it,
    and ``TestTimeoutError`` after ``timeout`` of real time; either way the
    effect is interrupted and its finalizers run first."""
    require_effect(eff, "eff")
    clock = TestClock(warning_delay=warning_delay)
    limit = None if timeout is None else to_seconds(timeout)
    try:
        return SyncRuntime(stall_timeout=stall_timeout).run(
            eff, Context.empty().add(Clock, clock).add(CurrentConsole, TestConsole()), timeout=limit
        )
    finally:
        clock.stop_warning()

Modules