Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move testing utils from core to testing #695

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 2 additions & 26 deletions src/ophyd_async/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,6 @@
from ._hdf_dataset import HDFDataset, HDFFile
from ._log import config_ophyd_async_logging
from ._mock_signal_backend import MockSignalBackend
from ._mock_signal_utils import (
callback_on_mock_put,
get_mock,
get_mock_put,
mock_puts_blocked,
reset_mock_put_calls,
set_mock_put_proceeds,
set_mock_value,
set_mock_values,
)
from ._protocol import AsyncConfigurable, AsyncReadable, AsyncStageable
from ._providers import (
AutoIncrementFilenameProvider,
Expand All @@ -53,14 +43,11 @@
)
from ._signal import (
Signal,
SignalConnector,
SignalR,
SignalRW,
SignalW,
SignalX,
assert_configuration,
assert_emitted,
assert_reading,
assert_value,
observe_signals_value,
observe_value,
set_and_wait_for_other_value,
Expand Down Expand Up @@ -123,14 +110,6 @@
"HDFFile",
"config_ophyd_async_logging",
"MockSignalBackend",
"callback_on_mock_put",
"get_mock",
"get_mock_put",
"mock_puts_blocked",
"reset_mock_put_calls",
"set_mock_put_proceeds",
"set_mock_value",
"set_mock_values",
"AsyncConfigurable",
"AsyncReadable",
"AsyncStageable",
Expand All @@ -150,14 +129,11 @@
"StandardReadable",
"StandardReadableFormat",
"Signal",
"SignalConnector",
"SignalR",
"SignalRW",
"SignalW",
"SignalX",
"assert_configuration",
"assert_emitted",
"assert_reading",
"assert_value",
"observe_value",
"observe_signals_value",
"set_and_wait_for_value",
Expand Down
3 changes: 1 addition & 2 deletions src/ophyd_async/core/_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from bluesky.protocols import HasName
from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop

from ._protocol import Connectable
from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection


Expand Down Expand Up @@ -61,7 +60,7 @@ async def connect_real(self, device: Device, timeout: float, force_reconnect: bo
await wait_for_connection(**coros)


class Device(HasName, Connectable):
class Device(HasName):
"""Common base class for all Ophyd Async Devices."""

_name: str = ""
Expand Down
28 changes: 0 additions & 28 deletions src/ophyd_async/core/_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,10 @@
from bluesky.protocols import HasName, Reading
from event_model import DataKey

from ._utils import DEFAULT_TIMEOUT

if TYPE_CHECKING:
from unittest.mock import Mock

from ._status import AsyncStatus


@runtime_checkable
class Connectable(Protocol):
@abstractmethod
async def connect(
self,
mock: bool | Mock = False,
timeout: float = DEFAULT_TIMEOUT,
force_reconnect: bool = False,
):
"""Connect self and all child Devices.

Contains a timeout that gets propagated to child.connect methods.

Parameters
----------
mock:
If True then use ``MockSignalBackend`` for all Signals
timeout:
Time to wait before failing with a TimeoutError.
force_reconnect:
Reconnect even if previous connect was successful.
"""


@runtime_checkable
class AsyncReadable(HasName, Protocol):
@abstractmethod
Expand Down
125 changes: 2 additions & 123 deletions src/ophyd_async/core/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import asyncio
import functools
import time
from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
from typing import Any, Generic, cast
from collections.abc import AsyncGenerator, Awaitable, Callable
from typing import Generic, cast

from bluesky.protocols import (
Locatable,
Expand All @@ -18,7 +18,6 @@
from ._device import Device, DeviceConnector
from ._mock_signal_backend import MockSignalBackend
from ._protocol import (
AsyncConfigurable,
AsyncReadable,
AsyncStageable,
Reading,
Expand Down Expand Up @@ -302,126 +301,6 @@ def soft_signal_r_and_setter(
return (signal, backend.set_value)


def _generate_assert_error_msg(name: str, expected_result, actual_result) -> str:
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
return (
f"Expected {WARNING}{name}{ENDC} to produce"
+ f"\n{FAIL}{expected_result}{ENDC}"
+ f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
)


async def assert_value(signal: SignalR[SignalDatatypeT], value: Any) -> None:
"""Assert a signal's value and compare it an expected signal.

Parameters
----------
signal:
signal with get_value.
value:
The expected value from the signal.

Notes
-----
Example usage::
await assert_value(signal, value)

"""
actual_value = await signal.get_value()
assert actual_value == value, _generate_assert_error_msg(
name=signal.name,
expected_result=value,
actual_result=actual_value,
)


async def assert_reading(
readable: AsyncReadable, expected_reading: Mapping[str, Reading]
) -> None:
"""Assert readings from readable.

Parameters
----------
readable:
Callable with readable.read function that generate readings.

reading:
The expected readings from the readable.

Notes
-----
Example usage::
await assert_reading(readable, reading)

"""
actual_reading = await readable.read()
assert expected_reading == actual_reading, _generate_assert_error_msg(
name=readable.name,
expected_result=expected_reading,
actual_result=actual_reading,
)


async def assert_configuration(
configurable: AsyncConfigurable,
configuration: Mapping[str, Reading],
) -> None:
"""Assert readings from Configurable.

Parameters
----------
configurable:
Configurable with Configurable.read function that generate readings.

configuration:
The expected readings from configurable.

Notes
-----
Example usage::
await assert_configuration(configurable configuration)

"""
actual_configurable = await configurable.read_configuration()
assert configuration == actual_configurable, _generate_assert_error_msg(
name=configurable.name,
expected_result=configuration,
actual_result=actual_configurable,
)


def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
"""Assert emitted document generated by running a Bluesky plan

Parameters
----------
Doc:
A dictionary

numbers:
expected emission in kwarg from

Notes
-----
Example usage::
assert_emitted(docs, start=1, descriptor=1,
resource=1, datum=1, event=1, stop=1)
"""
assert list(docs) == list(numbers), _generate_assert_error_msg(
name="documents",
expected_result=list(numbers),
actual_result=list(docs),
)
actual_numbers = {name: len(d) for name, d in docs.items()}
assert actual_numbers == numbers, _generate_assert_error_msg(
name="emitted",
expected_result=numbers,
actual_result=actual_numbers,
)


async def observe_value(
signal: SignalR[SignalDatatypeT],
timeout: float | None = None,
Expand Down
53 changes: 32 additions & 21 deletions src/ophyd_async/testing/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import asyncio
from ._assert import (
assert_configuration,
assert_emitted,
assert_reading,
assert_value,
)
from ._mock_signal_utils import (
callback_on_mock_put,
get_mock,
get_mock_put,
mock_puts_blocked,
reset_mock_put_calls,
set_mock_put_proceeds,
set_mock_value,
set_mock_values,
)
from ._wait_for_pending import wait_for_pending_wakeups


async def wait_for_pending_wakeups(max_yields=20, raise_if_exceeded=True):
"""Allow any ready asyncio tasks to be woken up.

Used in:

- Tests to allow tasks like ``set()`` to start so that signal
puts can be tested
- `observe_value` to allow it to be wrapped in `asyncio.wait_for`
with a timeout
"""
loop = asyncio.get_event_loop()
# If anything has called loop.call_soon or is scheduled a wakeup
# then let it run
for _ in range(max_yields):
await asyncio.sleep(0)
if not loop._ready: # type: ignore # noqa: SLF001
return
if raise_if_exceeded:
raise RuntimeError(f"Tasks still scheduling wakeups after {max_yields} yields")
__all__ = [
"assert_configuration",
"assert_emitted",
"assert_reading",
"assert_value",
"callback_on_mock_put",
"get_mock",
"get_mock_put",
"mock_puts_blocked",
"reset_mock_put_calls",
"set_mock_put_proceeds",
"set_mock_value",
"set_mock_values",
"wait_for_pending_wakeups",
]
Loading
Loading