diff --git a/docs/how-to/write-tests.md b/docs/how-to/write-tests.md index 73023744c9d..f7b710e0f71 100644 --- a/docs/how-to/write-tests.md +++ b/docs/how-to/write-tests.md @@ -121,6 +121,253 @@ async def test_my_device_read(sim_my_device: MyDevice, run_engine: RunEngine) -> ) ``` +## Mocking `ConfigClient` + +Beamlines and devices may depend on the daq-config-server `ConfigClient` to retrieve configuration files. These files are often stored on the DLS filesystem and contain configuration data that determines device behaviour. + +For example, an insertion device may use a calibration file containing polynomial coefficients that convert photon energy into insertion device gap for different polarisation modes. A device may retrieve this information using a `ConfigClient`: + +```python +from pathlib import Path + +from daq_config_server import ConfigClient + +from dodal.device_manager import DeviceManager +from dodal.devices.my_device import MyDevice + +LOOKUPTABLE_DIR = ( + "/dls_sw/iXX/software/gda/workspace_git/" + "gda-diamond.git/configurations/iXX/lookupTables" +) +LOOKUP_FILE_NAME = "JIDEnergy2GapCalibrations.csv" + +devices = DeviceManager() + + +@devices.fixture +def config_client() -> ConfigClient: + return ConfigClient() + + +@devices.factory() +def my_device(config_client: ConfigClient) -> MyDevice: + return MyDevice( + config_client=config_client, + path=Path(LOOKUPTABLE_DIR, LOOKUP_FILE_NAME), + ) +``` + +### Directly mocking return values + +If a test does not need to exercise file parsing logic, it is often simpler to mock the ConfigClient response directly rather than creating a test file and configuring a parser. + +```python +from unittest.mock import MagicMock + +from daq_config_server.models.lookup_tables.insertion_device import ( + UndulatorEnergyGapLookupTable, +) + +@pytest.fixture +def mock_config_client(): + mock_config_client = MagicMock() + mock_config_client.get_file_contents = MagicMock( + return_value=UndulatorEnergyGapLookupTable( + rows=[ + [5700, 5.4606], + [7000, 6.045], + [9700, 6.404], + ], + ) + ) + return mock_config_client +``` +This approach is appropriate when: + +* The test only cares about the behaviour of the device or component consuming the configuration data. +* The contents of the configuration file are not relevant to the test. +* The parsing logic is tested elsewhere. + +In these cases, mocking the return value directly avoids unnecessary coupling to file formats or test data files. + +Use the below method if you need the data to come from a file for a unit test. + +### Using the mock_config_client fixture to read files +Unit tests should not communicate with the real daq-config-server or read files from the DLS filesystem. + +Dodal provides a `mock_config_client` fixture in `dodal.testing.fixtures.config_client`. This fixture replaces the real service with a lightweight implementation that reads test files directly from the local filesystem. + +Most tests only need to depend on this fixture: + +```python +import pytest +from pathlib import Path + +from daq_config_server import ConfigClient +from dodal.devices.my_device import MyDevice +from ophyd_async.core import init_devices + + +@pytest.fixture +def my_device(mock_config_client: ConfigClient) -> MyDevice: + with init_devices(mock=True): + return MyDevice( + config_client=mock_config_client, + path=Path("tests/test_data/example_lookup_table.json"), + ) +``` + +### Default file parsing behaviour + +The mocked `ConfigClient.get_file_contents()` supports the same return types commonly used by production code: + +| Requested type | Behaviour | +| ---------------------- | ----------------------------------------------------------------- | +| `str` | Returns the raw file contents | +| `dict` | Parses the file as JSON and returns a dictionary | +| `ConfigModel` subclass | Parses the file as JSON and validates it using `model_validate()` | + +For example, if the requested return type is a `ConfigModel` subclass: + +```python +config_client.get_file_contents( + path, + desired_return_type=MyConfigModel, +) +``` + +the mock implementation will perform: + +```python +MyConfigModel.model_validate(json.loads(contents)) +``` + +### Registering a custom parser + +Some configuration file formats are not JSON-based and require custom parsing logic. For these cases, tests can register a parser using `MOCK_CONFIG_CLIENT_PATH_TO_MODEL_CONVERSION`. + +This registry maps a file path to a function that converts the file contents into a `ConfigModel`. + +For example: + +```python +import pytest +from daq_config_server import ConfigClient +from daq_config_server.models.lookup_tables.insertion_device import ( + parse_i09_hu_undulator_energy_gap_lut, +) + +from dodal.testing.fixtures.config_client import ( + MOCK_CONFIG_CLIENT_PATH_TO_MODEL_CONVERSION, +) +from tests.devices.beamlines.i09_1_shared.test_data import ( + TEST_HARD_UNDULATOR_LUT, +) + + +@pytest.fixture +def mock_config_client(mock_config_client: ConfigClient): + MOCK_CONFIG_CLIENT_PATH_TO_MODEL_CONVERSION[ + TEST_HARD_UNDULATOR_LUT + ] = parse_i09_hu_undulator_energy_gap_lut + + yield mock_config_client + + MOCK_CONFIG_CLIENT_PATH_TO_MODEL_CONVERSION.pop( + TEST_HARD_UNDULATOR_LUT, + None, + ) +``` + +When `get_file_contents()` is called for `TEST_HARD_UNDULATOR_LUT`, the registered parser will be used instead of the default JSON parsing behaviour. + +This mechanism exists as a temporary workaround for configuration formats that are not yet directly supported by daq-config-server. + +## Testing beamline module imports and device creation + +The tests in `tests/common/beamlines/test_device_instantiation.py` validate that every beamline module can be imported and that all device factories successfully construct devices in a test environment. + +During import and device creation, many beamlines load configuration files using either: + +* `ConfigClient.get_file_contents()` +* Module-level path constants that point to files on the DLS filesystem + +For example: + +```python +BEAMLINE_PARAMETERS_PATH = ( + "/dls_sw/i03/software/daq_configuration/json/beamlineParameters.json" +) +``` + +These files are not available in unit test environments and must not be accessed during tests. + +To allow beamline import and device creation tests to run without access to the DLS filesystem, the `module_and_devices_for_beamline` fixture performs two forms of mocking: + +1. Replaces `ConfigClient.get_file_contents()` with the `mock_config_client` implementation. +2. Replaces known module-level file path constants with paths to test data files. + +### Mocking `ConfigClient` + +Before importing the beamline module, the fixture replaces the production implementation: + +```python +ConfigClient.get_file_contents = mock_config_client.get_file_contents +``` + +This ensures that any configuration requested through the daq-config-server client is loaded from local test files rather than the real service. + +### Mocking beamline file paths + +Some beamlines contain module-level constants that directly reference files on the DLS filesystem. These constants are replaced after module import using the `MOCK_ATTRIBUTES_TABLE`. + +For example: + +```python +MOCK_ATTRIBUTES_TABLE = { + "i03": { + "BEAMLINE_PARAMETERS_PATH": TEST_BEAMLINE_PARAMETERS_TXT, + "DAQ_CONFIGURATION_PATH": MOCK_DAQ_CONFIG_PATH, + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, +} +``` + +When the `i03` beamline is tested, these attributes are overwritten with paths to local test files: + +```python +mock_beamline_module_filepaths("i03", beamline_module) +``` + +This allows device factories to load representative configuration data without accessing the real DLS filesystem. + +### Adding support for a new beamline + +If the device creation tests fail because a beamline attempts to access a file under `/dls` or `/dls_sw`, identify the module attribute containing the path and add an entry to `MOCK_ATTRIBUTES_TABLE`. + +For example: + +```python +MOCK_ATTRIBUTES_TABLE = { + ... + "iXX: { + "MY_CONFIGURATION_FILE": TEST_MY_CONFIGURATION_FILE + }, + ... +} +``` + +The replacement file should be committed to the test suite and contain the minimum configuration required for the beamline to initialise successfully. + +As a rule of thumb: + +* Use `mock_config_client` when configuration is loaded through `ConfigClient`. +* Use `MOCK_ATTRIBUTES_TABLE` when a beamline module contains hard-coded filesystem paths. +* If both mechanisms are used by a beamline, both may need to be configured for the tests to pass. + + + ## Test performance and reliability Dodal has well over 1000 unit tests and developers will run the full unit test suite frequently on their local diff --git a/pyproject.toml b/pyproject.toml index 36212985b73..af59ab87c5e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "requests", "graypy", "pydantic>=2.0", - "opencv-python-headless", # For pin-tip detection. + "opencv-python-headless", # For pin-tip detection. "numpy", "aiofiles", "aiohttp", @@ -32,8 +32,8 @@ dependencies = [ "scanspec>=0.7.3", "pyzmq==27.1.0", "deepdiff", - "daq-config-server >= 1.3.6", # For getting Configuration settings. - "mysql-connector-python == 9.5.0", # Can unpin once https://github.com/DiamondLightSource/ispyb-api/pull/244 is merged and released + "daq-config-server >= 1.3.6", # For getting Configuration settings. + "mysql-connector-python == 9.5.0", # Can unpin once https://github.com/DiamondLightSource/ispyb-api/pull/244 is merged and released ] dynamic = ["version"] @@ -113,9 +113,10 @@ typeCheckingMode = "standard" reportMissingImports = false # Ignore missing stubs in imported modules [tool.pytest.ini_options] -# Run pytest with all our checkers, and don't spam us with massive tracebacks on error +# Run pytest with all our checkers asyncio_mode = "auto" timeout = 1 +timeout_method = "signal" # Alllow us to see full stack trace of where it timed out to make debugging easier. markers = [ "requires: marks tests as requiring other infrastructure", "skip_in_pycharm: marks test as not working in pycharm testrunner", diff --git a/src/dodal/beamlines/i09_1_shared.py b/src/dodal/beamlines/i09_1_shared.py index b41a6dd08ba..8b31b49d37b 100644 --- a/src/dodal/beamlines/i09_1_shared.py +++ b/src/dodal/beamlines/i09_1_shared.py @@ -1,6 +1,5 @@ from daq_config_server import ConfigClient -from dodal.common.beamlines.beamline_utils import get_config_client, set_config_client from dodal.device_manager import DeviceManager from dodal.devices.beamlines.i09_1_shared import ( HardEnergy, @@ -30,10 +29,14 @@ devices = DeviceManager() -set_config_client(ConfigClient()) LOOK_UPTABLE_FILE = "/dls_sw/i09-1/software/gda/workspace_git/gda-diamond.git/configurations/i09-1-shared/lookupTables/IIDCalibrationTable.txt" +@devices.fixture +def iconfig_client() -> ConfigClient: + return ConfigClient() + + @devices.factory() def psi1() -> HutchShutter: return HutchShutter(I_PREFIX.beamline_prefix) @@ -60,12 +63,14 @@ def ienergy_order() -> UndulatorOrder: @devices.factory() def iidenergy( - ienergy_order: UndulatorOrder, iid: UndulatorInMm + ienergy_order: UndulatorOrder, + iid: UndulatorInMm, + iconfig_client: ConfigClient, ) -> HardInsertionDeviceEnergy: return HardInsertionDeviceEnergy( undulator_order=ienergy_order, undulator=iid, - config_server=get_config_client(), + config_server=iconfig_client, filepath=LOOK_UPTABLE_FILE, gap_to_energy_func=calculate_energy_i09_hu, energy_to_gap_func=calculate_gap_i09_hu, diff --git a/src/dodal/beamlines/i09_2_shared.py b/src/dodal/beamlines/i09_2_shared.py index 930cd54253d..1bf31496610 100644 --- a/src/dodal/beamlines/i09_2_shared.py +++ b/src/dodal/beamlines/i09_2_shared.py @@ -25,7 +25,6 @@ from dodal.devices.pgm import PlaneGratingMonochromator from dodal.utils import BeamlinePrefix, get_beamline_name -J09_CONF_CLIENT = ConfigClient(url="https://daq-config.diamond.ac.uk") LOOK_UPTABLE_DIR = "/dls_sw/i09-2/software/gda/workspace_git/gda-diamond.git/configurations/i09-2-shared/lookupTables/" GAP_LOOKUP_FILE_NAME = "JIDEnergy2GapCalibrations.csv" PHASE_LOOKUP_FILE_NAME = "JIDEnergy2PhaseCalibrations.csv" @@ -37,6 +36,11 @@ devices = DeviceManager() +@devices.fixture +def jconfig_client() -> ConfigClient: + return ConfigClient() + + @devices.factory() def psk1() -> HutchShutter: return HutchShutter(K_PREFIX.beamline_prefix) @@ -75,18 +79,19 @@ def jid(jgap: UndulatorGap, jphase: UndulatorPhaseAxes) -> Apple2[UndulatorPhase @devices.factory() def jidcontroller( jid: Apple2[UndulatorPhaseAxes], + jconfig_client: ConfigClient, ) -> Apple2EnforceLHMoveController[UndulatorPhaseAxes]: """J09 insertion device controller.""" return Apple2EnforceLHMoveController[UndulatorPhaseAxes]( apple2=jid, gap_energy_motor_lut=ConfigServerEnergyMotorLookup( lut_config=LookupTableColumnConfig(poly_deg=J09_GAP_POLY_DEG_COLUMNS), - config_client=J09_CONF_CLIENT, + config_client=jconfig_client, path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME), ), phase_energy_motor_lut=ConfigServerEnergyMotorLookup( lut_config=LookupTableColumnConfig(poly_deg=J09_PHASE_POLY_DEG_COLUMNS), - config_client=J09_CONF_CLIENT, + config_client=jconfig_client, path=Path(LOOK_UPTABLE_DIR, PHASE_LOOKUP_FILE_NAME), ), units="keV", diff --git a/src/dodal/beamlines/i10_shared.py b/src/dodal/beamlines/i10_shared.py index 6367c135c95..4e479916ec5 100644 --- a/src/dodal/beamlines/i10_shared.py +++ b/src/dodal/beamlines/i10_shared.py @@ -54,6 +54,11 @@ devices = DeviceManager() +@devices.fixture +def config_client() -> ConfigClient: + return ConfigClient() + + @devices.factory() def synchrotron() -> Synchrotron: return Synchrotron() @@ -86,8 +91,6 @@ def switching_mirror() -> XYZPiezoCollimatingMirror: """ID""" -I10_CONF_CLIENT = ConfigClient(url="https://daq-config.diamond.ac.uk") - LOOK_UPTABLE_DIR = "/dls_sw/i10/software/gda/workspace_git/gda-diamond.git/configurations/i10-shared/lookupTables/" @@ -126,16 +129,16 @@ def idd( @devices.factory() -def idd_controller(idd: I10Apple2) -> I10Apple2Controller: +def idd_controller(idd: I10Apple2, config_client: ConfigClient) -> I10Apple2Controller: """I10 downstream insertion device controller.""" source = Source(column="Source", value="idd") idd_gap_energy_motor_lut = ConfigServerEnergyMotorLookup( - config_client=I10_CONF_CLIENT, + config_client=config_client, lut_config=LookupTableColumnConfig(source=source), path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE), ) idd_phase_energy_motor_lut = ConfigServerEnergyMotorLookup( - config_client=I10_CONF_CLIENT, + config_client=config_client, lut_config=LookupTableColumnConfig(source=source), path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE), ) @@ -206,16 +209,16 @@ def idu( @devices.factory() -def idu_controller(idu: I10Apple2) -> I10Apple2Controller: +def idu_controller(idu: I10Apple2, config_client: ConfigClient) -> I10Apple2Controller: """I10 upstream insertion device controller.""" source = Source(column="Source", value="idu") idu_gap_energy_motor_lut = ConfigServerEnergyMotorLookup( - config_client=I10_CONF_CLIENT, + config_client=config_client, lut_config=LookupTableColumnConfig(source=source), path=Path(LOOK_UPTABLE_DIR, DEFAULT_GAP_FILE), ) idu_phase_energy_motor_lut = ConfigServerEnergyMotorLookup( - config_client=I10_CONF_CLIENT, + config_client=config_client, lut_config=LookupTableColumnConfig(source=source), path=Path(LOOK_UPTABLE_DIR, DEFAULT_PHASE_FILE), ) diff --git a/src/dodal/beamlines/i18.py b/src/dodal/beamlines/i18.py index d662e04a007..b18114c2d89 100644 --- a/src/dodal/beamlines/i18.py +++ b/src/dodal/beamlines/i18.py @@ -5,10 +5,6 @@ from ophyd_async.core import PathProvider from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - get_config_client, - set_config_client, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.visit import ( LocalDirectoryServiceClient, @@ -53,7 +49,9 @@ def path_provider() -> PathProvider: ) -set_config_client(ConfigClient()) +@devices.fixture +def config_client() -> ConfigClient: + return ConfigClient() @devices.factory() @@ -62,10 +60,8 @@ def synchrotron() -> Synchrotron: @devices.factory() -def undulator() -> UndulatorInKeV: - return UndulatorInKeV( - f"{PREFIX.insertion_prefix}-MO-SERVC-01:", get_config_client() - ) +def undulator(config_client: ConfigClient) -> UndulatorInKeV: + return UndulatorInKeV(f"{PREFIX.insertion_prefix}-MO-SERVC-01:", config_client) # See https://github.com/DiamondLightSource/dodal/issues/1180 diff --git a/src/dodal/beamlines/i21.py b/src/dodal/beamlines/i21.py index 78396c2491f..dca6873ffc6 100644 --- a/src/dodal/beamlines/i21.py +++ b/src/dodal/beamlines/i21.py @@ -38,7 +38,7 @@ I21_PHASE_POLY_DEG_COLUMNS = ["b"] I21_GRATING_COLUMNS = "Grating" -I21_CONF_CLIENT = ConfigClient(url="https://daq-config.diamond.ac.uk") +I21_CONF_CLIENT = ConfigClient() LOOK_UPTABLE_DIR = "/dls_sw/i21/software/gda/workspace_git/gda-diamond.git/configurations/i21-config/lookupTables/" GAP_LOOKUP_FILE_NAME = "IDEnergy2GapCalibrations.csv" PHASE_LOOKUP_FILE_NAME = "IDEnergy2PhaseCalibrations.csv" diff --git a/src/dodal/beamlines/k07.py b/src/dodal/beamlines/k07.py index b7c27de6610..f7e184316eb 100644 --- a/src/dodal/beamlines/k07.py +++ b/src/dodal/beamlines/k07.py @@ -22,9 +22,6 @@ devices = DeviceManager() - -K07_CONF_CLIENT = ConfigClient(url="https://daq-config.diamond.ac.uk") - LOOK_UPTABLE_DIR = "/dls_sw/k07/software/gda/workspace_git/gda-diamond.git/configurations/k07/lookupTables/" GAP_LOOKUP_FILE_NAME = "JIDEnergy2GapCalibrations.csv" PHASE_LOOKUP_FILE_NAME = "JIDEnergy2PhaseCalibrations.csv" @@ -37,6 +34,11 @@ set_utils_beamline(BL) +@devices.fixture +def config_client() -> ConfigClient: + return ConfigClient() + + @devices.factory() def synchrotron() -> Synchrotron: return Synchrotron() @@ -92,20 +94,21 @@ def id( @devices.factory(skip=True) def id_controller( id: Apple2[UndulatorPhaseAxes], + config_client: ConfigClient, ) -> Apple2EnforceLHMoveController[UndulatorPhaseAxes]: """I21 insertion device controller.""" return Apple2EnforceLHMoveController[UndulatorPhaseAxes]( apple2=id, gap_energy_motor_lut=ConfigServerEnergyMotorLookup( lut_config=LookupTableColumnConfig(grating=K07_GRATING_COLUMNS), - config_client=K07_CONF_CLIENT, + config_client=config_client, path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME), ), phase_energy_motor_lut=ConfigServerEnergyMotorLookup( lut_config=LookupTableColumnConfig( grating=K07_GRATING_COLUMNS, poly_deg=K07_PHASE_POLY_DEG_COLUMNS ), - config_client=K07_CONF_CLIENT, + config_client=config_client, path=Path(LOOK_UPTABLE_DIR, GAP_LOOKUP_FILE_NAME), ), units="eV", diff --git a/src/dodal/beamlines/p38.py b/src/dodal/beamlines/p38.py index 7e7800fc312..e2c0ccde6bb 100644 --- a/src/dodal/beamlines/p38.py +++ b/src/dodal/beamlines/p38.py @@ -7,10 +7,6 @@ from ophyd_async.epics.adcore import ADWriterFactory from ophyd_async.fastcs.panda import HDFPanda -from dodal.common.beamlines.beamline_utils import ( - get_config_client, - set_config_client, -) from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.common.beamlines.device_helpers import HDF5_SUFFIX from dodal.common.crystal_metadata import ( @@ -54,7 +50,9 @@ def path_provider() -> PathProvider: ) -set_config_client(ConfigClient()) +@devices.fixture +def config_client() -> ConfigClient: + return ConfigClient() @devices.factory() @@ -168,10 +166,10 @@ def dcm() -> DCM: @devices.factory(mock=True) -def undulator() -> UndulatorInKeV: +def undulator(config_client: ConfigClient) -> UndulatorInKeV: return UndulatorInKeV( f"{PREFIX.insertion_prefix}-MO-SERVC-01:", - get_config_client(), + config_client, poles=80, length=2.0, ) diff --git a/src/dodal/testing/fixtures/config_client.py b/src/dodal/testing/fixtures/config_client.py new file mode 100644 index 00000000000..518d728b072 --- /dev/null +++ b/src/dodal/testing/fixtures/config_client.py @@ -0,0 +1,96 @@ +import json +from collections.abc import Callable +from functools import partial +from pathlib import Path +from typing import Any, TypeVar +from unittest.mock import MagicMock + +import pytest +from daq_config_server import ConfigClient +from daq_config_server.models import ConfigModel +from pydantic import TypeAdapter + +T = TypeVar("T", str, dict, ConfigModel) + + +TModel = TypeVar("TModel", bound=ConfigModel) + + +ConverterDict = dict[str, Callable[[str], ConfigModel]] + + +def _mock_config_server_get_file_contents( + file_path: str | Path, + desired_return_type: type[T] = str, + reset_cached_result: bool = True, + force_parser: Callable[[str], Any] | None = None, + mock_data_converters: ConverterDict | None = None, +) -> T: + """Mocks getting a file from the config server by reading it directly. + + Args: + file_path (str | Path): Filepath of the file to read + desired_return_type (type[T], optional): Type to convert the file to. Defaults to str. + reset_cached_result (bool, optional): Whether or not to use the config server's cached result. + Does nothing here as we don't cache. Defaults to True. + force_parser (Callable[[str], Any] | None, optional): Use a certain converter function. + Only needed for the interim where the converter exists but the config server has not + been redeployed. Defaults to None. + mock_data_converters (ConverterDict | None): If there is a converter in here matching + the file path then that will be used for conversion. + + Raises: + ValueError: Raised if an invalid type is requested + + Returns: + T: The contents of the config file. + """ + requested_file = Path(file_path) + + # Minimal logic required for unit tests + with requested_file.open("r") as f: + contents = f.read() + + if force_parser: + return TypeAdapter(desired_return_type).validate_python(force_parser(contents)) + + if desired_return_type is str: + return contents # type: ignore + + if desired_return_type is dict: + return json.loads(contents) + + if issubclass(desired_return_type, ConfigModel): + if not mock_data_converters: + mock_data_converters = {} + + if str(requested_file) in mock_data_converters: + callable = mock_data_converters[str(requested_file)] + return callable(contents) # type: ignore + + return desired_return_type.model_validate(json.loads(contents)) + + raise ValueError(f"Invalid return type requested: {desired_return_type}") + + +def _mock_config_client_factory(mock_data_converters: ConverterDict) -> ConfigClient: + # Don't actually talk to central service during unit tests, and reset caches between test + mock_config_client = ConfigClient() + mock_config_client.get_file_contents = MagicMock(spec=["get_file_contents"]) + mock_config_client.get_file_contents.side_effect = partial( + _mock_config_server_get_file_contents, + mock_data_converters=mock_data_converters, + ) + return mock_config_client + + +@pytest.fixture +def mock_config_client() -> ConfigClient: + return _mock_config_client_factory({}) + + +@pytest.fixture +def mock_config_client_with_data( + mock_config_client_data: ConverterDict, +) -> ConfigClient: + return _mock_config_client_factory(mock_config_client_data) diff --git a/src/dodal/testing/fixtures/config_server.py b/src/dodal/testing/fixtures/config_server.py deleted file mode 100644 index ba84eb050ca..00000000000 --- a/src/dodal/testing/fixtures/config_server.py +++ /dev/null @@ -1,60 +0,0 @@ -import json -from collections.abc import Callable -from pathlib import Path -from typing import Any, TypeVar -from unittest.mock import patch - -import pytest -from daq_config_server.models import ConfigModel -from pydantic import TypeAdapter - -T = TypeVar("T", str, dict, ConfigModel) - - -def fake_config_server_get_file_contents( - filepath: str | Path, - desired_return_type: type[T] = str, - reset_cached_result: bool = True, - force_parser: Callable[[str], Any] | None = None, -) -> T: - """Fakes getting a file from the config server by reading it directly. - - Args: - filepath (str | Path): Filepath of the file to read - desired_return_type (type[T], optional): Type to convert the file to. Defaults to str. - reset_cached_result (bool, optional): Whether or not to use the config server's cached result. - Does nothing here as we don't cache. Defaults to True. - force_parser (Callable[[str], Any] | None, optional): Use a certain converter function. - Only needed for the interim where the converter exists but the config server has not - been redeployed. Defaults to None. - - Raises: - ValueError: Raised if an invalid type is requested - - Returns: - T: The contents of the config file. - """ - filepath = Path(filepath) - # Minimal logic required for unit tests - with filepath.open("r") as f: - contents = f.read() - if force_parser: - return TypeAdapter(desired_return_type).validate_python(force_parser(contents)) - if desired_return_type is str: - return contents # type: ignore - if desired_return_type is dict: - return json.loads(contents) - if issubclass(desired_return_type, ConfigModel): - return desired_return_type.model_validate(json.loads(contents)) - raise ValueError("Invalid return type requested") - - -@pytest.fixture(autouse=True) -def mock_config_server(): - # Don't actually talk to central service during unit tests, and reset caches between test - - with patch( - "daq_config_server.app.client.ConfigClient.get_file_contents", - side_effect=fake_config_server_get_file_contents, - ): - yield diff --git a/src/dodal/testing/fixtures/devices/apple2.py b/src/dodal/testing/fixtures/devices/apple2.py index a8d06cedc14..e5baee50ee7 100644 --- a/src/dodal/testing/fixtures/devices/apple2.py +++ b/src/dodal/testing/fixtures/devices/apple2.py @@ -1,7 +1,4 @@ -from unittest.mock import MagicMock - import pytest -from daq_config_server import ConfigClient from ophyd_async.core import ( init_devices, set_mock_value, @@ -19,20 +16,7 @@ UndulatorLockedPhaseAxes, ) - -@pytest.fixture -def mock_config_client() -> ConfigClient: - mock_config_client = ConfigClient() - - mock_config_client.get_file_contents = MagicMock(spec=["get_file_contents"]) - - def my_side_effect(file_path, reset_cached_result) -> str: - assert reset_cached_result is True - with open(file_path) as f: - return f.read() - - mock_config_client.get_file_contents.side_effect = my_side_effect - return mock_config_client +pytest_plugins = ["dodal.testing.fixtures.config_client"] @pytest.fixture diff --git a/src/dodal/testing/fixtures/devices/hard_undulator.py b/src/dodal/testing/fixtures/devices/hard_undulator.py deleted file mode 100644 index 4f8ae689b4b..00000000000 --- a/src/dodal/testing/fixtures/devices/hard_undulator.py +++ /dev/null @@ -1,43 +0,0 @@ -from unittest.mock import MagicMock - -import pytest -from daq_config_server import ConfigClient -from daq_config_server.models.lookup_tables import GenericLookupTable - -from dodal.devices.beamlines.i09_1_shared.hard_undulator_functions import ( - I09_HU_UNDULATOR_LUT_COLUMN_NAMES, -) - -lut = GenericLookupTable( - column_names=I09_HU_UNDULATOR_LUT_COLUMN_NAMES, - rows=[ - [1, 3.00089, 0.98928, 2.12, 3.05, 14.265, 23.72, 0.0], - [2, 3.04129, 1.02504, 2.5, 2.8, 5.05165, 8.88007, 0.0], - [3, 3.05798, 1.03065, 2.4, 4.3, 5.2, 8.99036, 0.0], - [4, 3.03635, 1.02332, 3.2, 5.7, 5.26183, 8.9964, 0.0], - [5, 3.06334, 1.03294, 4.0, 7.2, 5.22735, 9.02065, 0.0], - [6, 3.04963, 1.02913, 4.7, 8.6, 5.13939, 9.02527, 0.0], - [7, 3.06515, 1.03339, 5.5, 10.1, 5.12684, 9.02602, 0.0], - [8, 3.05775, 1.03223, 6.3, 11.5, 5.16289, 9.02873, 0.0], - [9, 3.06829, 1.03468, 7.1, 13.0, 5.16357, 9.03049, 0.0], - [10, 3.06164, 1.03328, 7.9, 14.4, 5.17205, 9.02845, 0.0], - [11, 3.07056, 1.03557, 8.6, 15.9, 5.1135, 9.0475, 0.0], - [12, 3.06627, 1.03482, 9.4, 17.3, 5.12051, 9.02826, 0.0], - [13, 3.07176, 1.03623, 10.2, 18.3, 5.13027, 8.8494, 0.0], - [14, 3.06964, 1.03587, 11.0, 18.3, 5.13985, 8.30146, 0.0], - [15, 3.06515, 1.03391, 11.8, 18.3, 5.14643, 7.8238, 0.0], - ], -) - - -@pytest.fixture -def mock_config_client() -> ConfigClient: - mock_config_client = ConfigClient() - mock_config_client.get_file_contents = MagicMock(spec=["get_file_contents"]) - - def my_side_effect(file_path, desired_return_type, reset_cached_result): - assert reset_cached_result is True - return lut - - mock_config_client.get_file_contents.side_effect = my_side_effect - return mock_config_client diff --git a/system_tests/conftest.py b/system_tests/conftest.py index 06fee26e905..c0b76377f5a 100644 --- a/system_tests/conftest.py +++ b/system_tests/conftest.py @@ -1,5 +1,5 @@ -# Add run_engine to be used in tests +# Add run_engine and mock_config_client to be used in system tests pytest_plugins = [ "dodal.testing.fixtures.run_engine", - "dodal.testing.fixtures.config_server", + "dodal.testing.fixtures.config_client", ] diff --git a/system_tests/test_oav_system.py b/system_tests/test_oav_system.py index 30f04a2fc0a..5b5885b42bb 100644 --- a/system_tests/test_oav_system.py +++ b/system_tests/test_oav_system.py @@ -2,7 +2,6 @@ import pytest from bluesky.run_engine import RunEngine from daq_config_server import ConfigClient -from ophyd_async.core import init_devices from tests.devices.oav.test_data import TEST_OAV_ZOOM_LEVELS from dodal.devices.oav.oav_detector import OAV, OAVConfig @@ -14,14 +13,6 @@ TEST_GRID_NUM_BOXES_Y = 6 -@pytest.fixture -async def oav() -> OAV: - oav_config = OAVConfig(TEST_OAV_ZOOM_LEVELS, ConfigClient("")) - async with init_devices(connect=True): - oav = OAV("", config=oav_config, name="oav") - return oav - - def take_snapshot_with_grid(oav: OAV, snapshot_filename, snapshot_directory): yield from bps.abs_set(oav.grid_snapshot.top_left_x, TEST_GRID_TOP_LEFT_X) yield from bps.abs_set(oav.grid_snapshot.top_left_y, TEST_GRID_TOP_LEFT_Y) @@ -35,9 +26,9 @@ def take_snapshot_with_grid(oav: OAV, snapshot_filename, snapshot_directory): # We need to find a better way of integrating this, see https://github.com/DiamondLightSource/mx-bluesky/issues/183 @pytest.mark.skip(reason="Don't want to actually take snapshots during testing.") -def test_grid_overlay(run_engine: RunEngine): +def test_grid_overlay(run_engine: RunEngine, mock_config_client: ConfigClient): beamline = "BL03I" - oav_params = OAVConfig(TEST_OAV_ZOOM_LEVELS, ConfigClient("")) + oav_params = OAVConfig(TEST_OAV_ZOOM_LEVELS, mock_config_client) oav = OAV(name="oav", prefix=f"{beamline}", config=oav_params) snapshot_filename = "snapshot" snapshot_directory = "." diff --git a/tests/beamlines/test_i24.py b/tests/beamlines/test_i24.py deleted file mode 100644 index e32bb47c072..00000000000 --- a/tests/beamlines/test_i24.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest - -from dodal.devices.beamlines.i24.vgonio import VerticalGoniometer - - -@pytest.mark.parametrize("module_and_devices_for_beamline", ["i24"], indirect=True) -def test_device_creation(module_and_devices_for_beamline): - _, devices, exceptions = module_and_devices_for_beamline - assert not exceptions - vgonio: VerticalGoniometer = devices["vgonio"] # type: ignore - - assert vgonio._name == "vgonio" - assert vgonio.omega._name == "vgonio-omega" diff --git a/tests/common/beamlines/test_beamline_parameters.py b/tests/common/beamlines/test_beamline_parameters.py index 42e7f88d69d..5a0faeb28dd 100644 --- a/tests/common/beamlines/test_beamline_parameters.py +++ b/tests/common/beamlines/test_beamline_parameters.py @@ -27,8 +27,8 @@ def patch_beamline_parameter_paths(): @pytest.fixture(autouse=True) -def always_set_config_client(): - set_config_client(ConfigClient("test")) +def always_set_config_client(mock_config_client: ConfigClient): + set_config_client(mock_config_client) def test_beamline_parameters(): diff --git a/tests/common/beamlines/test_device_instantiation.py b/tests/common/beamlines/test_device_instantiation.py index e9ce27fd437..6a1cbeaef5c 100644 --- a/tests/common/beamlines/test_device_instantiation.py +++ b/tests/common/beamlines/test_device_instantiation.py @@ -1,44 +1,25 @@ from typing import Any import pytest -from daq_config_server import ConfigClient from ophyd_async.core import NotConnectedError from dodal.beamlines import all_beamline_modules -from dodal.common.beamlines.beamline_utils import clear_config_client, set_config_client from dodal.device_manager import DeviceManager from dodal.utils import BLUESKY_PROTOCOLS, make_all_devices -from tests.test_data import I04_BEAMLINE_PARAMETERS, TEST_BEAMLINE_PARAMETERS_TXT def follows_bluesky_protocols(obj: Any) -> bool: return any(isinstance(obj, protocol) for protocol in BLUESKY_PROTOCOLS) -@pytest.fixture(autouse=True) -def patch_config_paths(monkeypatch): - monkeypatch.setattr( - "dodal.beamlines.i03.BEAMLINE_PARAMETERS_PATH", - TEST_BEAMLINE_PARAMETERS_TXT, - ) - monkeypatch.setattr( - "dodal.beamlines.i04.BEAMLINE_PARAMETERS_PATH", - I04_BEAMLINE_PARAMETERS, - ) - - -@pytest.fixture(autouse=True) -def reset_config_client(): - set_config_client(ConfigClient("")) - yield - clear_config_client() - - @pytest.mark.parametrize( "module_and_devices_for_beamline", set(all_beamline_modules()), indirect=True, ) +# Increase timeout for this specific test as running individually can take over a second +# to import everything so causes tests to fail. +@pytest.mark.timeout(2) def test_device_creation(module_and_devices_for_beamline): """Ensures that for every beamline all device factories are using valid args and creating types that conform to Bluesky protocols. @@ -61,6 +42,9 @@ def test_device_creation(module_and_devices_for_beamline): set(all_beamline_modules()), indirect=True, ) +# Increase timeout for this specific test as running individually can take over a second +# to import everything so causes tests to fail. +@pytest.mark.timeout(2) def test_devices_are_identical(module_and_devices_for_beamline): """Ensures that for every beamline all device functions prevent duplicate instantiation. diff --git a/tests/conftest.py b/tests/conftest.py index 7f4c6a77ce3..b8031503d74 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,10 @@ from unittest.mock import MagicMock, patch import pytest +from daq_config_server import ConfigClient from ophyd_async.core import PathProvider -from dodal.common.beamlines import beamline_parameters, beamline_utils +from dodal.common.beamlines import beamline_utils from dodal.common.beamlines.beamline_utils import ( clear_config_client, clear_path_provider, @@ -29,45 +30,69 @@ collect_factories, make_all_devices, ) -from tests.devices.beamlines.i10.test_data import LOOKUP_TABLE_PATH from tests.devices.oav.test_data import TEST_DISPLAY_CONFIG, TEST_OAV_ZOOM_LEVELS from tests.devices.test_daq_configuration import MOCK_DAQ_CONFIG_PATH from tests.devices.test_data import TEST_LUT_TXT -from tests.test_data import ( - I04_BEAMLINE_PARAMETERS, -) - -MOCK_PATHS = [ - ("DAQ_CONFIGURATION_PATH", MOCK_DAQ_CONFIG_PATH), - ("ZOOM_PARAMS_FILE", TEST_OAV_ZOOM_LEVELS), - ("DISPLAY_CONFIG", TEST_DISPLAY_CONFIG), - ("LOOK_UPTABLE_DIR", LOOKUP_TABLE_PATH), - ("SHARED_CONFIG_PATH", MOCK_DAQ_CONFIG_PATH), -] -MOCK_ATTRIBUTES_TABLE = { - "i03": MOCK_PATHS, - "i10_optics": MOCK_PATHS, - "i04": MOCK_PATHS, - "i19_1": MOCK_PATHS, - "i19_2": MOCK_PATHS, - "i19_optics": MOCK_PATHS, - "i23": MOCK_PATHS, - "i24": MOCK_PATHS, - "aithre": MOCK_PATHS, +from tests.test_data import I04_BEAMLINE_PARAMETERS, TEST_BEAMLINE_PARAMETERS_TXT + +MOCK_ATTRIBUTES_TABLE: dict[str, dict[str, str]] = { + "i03": { + "BEAMLINE_PARAMETERS_PATH": TEST_BEAMLINE_PARAMETERS_TXT, + "DAQ_CONFIGURATION_PATH": MOCK_DAQ_CONFIG_PATH, + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, + "i04": { + "BEAMLINE_PARAMETERS_PATH": I04_BEAMLINE_PARAMETERS, + "DAQ_CONFIGURATION_PATH": MOCK_DAQ_CONFIG_PATH, + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, + "i19_1": { + "SHARED_CONFIG_PATH": MOCK_DAQ_CONFIG_PATH, + "DAQ_CONFIGURATION_PATH": MOCK_DAQ_CONFIG_PATH, + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, + "i19_2": { + "SHARED_CONFIG_PATH": MOCK_DAQ_CONFIG_PATH, + }, + "i19_optics": { + "DAQ_CONFIGURATION_PATH": MOCK_DAQ_CONFIG_PATH, + }, + "aithre": { + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + }, + "i23": { + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, + "i24": { + "ZOOM_PARAMS_FILE": TEST_OAV_ZOOM_LEVELS, + "DISPLAY_CONFIG": TEST_DISPLAY_CONFIG, + }, } BANNED_PATHS = [Path("/dls"), Path("/dls_sw")] environ["DODAL_TEST_MODE"] = "true" -# Add run_engine and util fixtures to be used in tests +# Add run_engine, util fixtures, and mock_config_client to be used in tests pytest_plugins = [ "dodal.testing.fixtures.run_engine", "dodal.testing.fixtures.utils", - "dodal.testing.fixtures.config_server", + "dodal.testing.fixtures.config_client", ] +def is_path_banned(path: Path, banned_paths: list[Path]) -> bool: + resolved = path.resolve() + return resolved.is_absolute() and any( + resolved.is_relative_to(banned) for banned in banned_paths + ) + + @pytest.fixture(autouse=True) def reset_path_provider(): yield @@ -77,18 +102,28 @@ def reset_path_provider(): @pytest.fixture(autouse=True) def patch_open_to_prevent_dls_reads_in_tests(): unpatched_open = open + unpatched_path_open = Path.open - def patched_open(*args, **kwargs): - requested_path = Path(args[0]) - if requested_path.is_absolute(): - for p in BANNED_PATHS: - assert not requested_path.is_relative_to(p), ( - f"Attempt to open {requested_path} from inside a unit test" - ) - return unpatched_open(*args, **kwargs) + def check_path(path: str | Path): + requested_path = Path(path) + if is_path_banned(requested_path, BANNED_PATHS): + raise AssertionError( + f"Attempt to open {requested_path} from inside a unit test" + ) + + def patched_open(file, *args, **kwargs): + check_path(file) + return unpatched_open(file, *args, **kwargs) + + def patched_path_open(self: Path, *args, **kwargs): + check_path(self) + return unpatched_path_open(self, *args, **kwargs) - with patch("builtins.open", side_effect=patched_open): - yield [] + with ( + patch("builtins.open", side_effect=patched_open), + patch.object(Path, "open", patched_path_open), + ): + yield def pytest_runtest_setup(item): @@ -125,8 +160,16 @@ async def static_path_provider( @pytest.fixture(scope="function") -def module_and_devices_for_beamline(request: pytest.FixtureRequest): +def module_and_devices_for_beamline( + mock_config_client: ConfigClient, request: pytest.FixtureRequest +): + # Swap all ConfigClient get_file_contents instances with the mock one method. + # Open local files for tests rather than from service. + original_config_client = ConfigClient.get_file_contents + ConfigClient.get_file_contents = mock_config_client.get_file_contents # type: ignore + beamline = request.param + with patch.dict(os.environ, {"BEAMLINE": beamline}, clear=True): bl_mod = importlib.import_module("dodal.beamlines." + beamline) mock_beamline_module_filepaths(beamline, bl_mod) @@ -151,11 +194,13 @@ def module_and_devices_for_beamline(request: pytest.FixtureRequest): factory.cache_clear() del bl_mod + # Set back the ConfigClient instance with the original one. + ConfigClient.get_file_contents = original_config_client + def mock_beamline_module_filepaths(bl_name: str, bl_module: ModuleType): if mock_attributes := MOCK_ATTRIBUTES_TABLE.get(bl_name): - [bl_module.__setattr__(attr[0], attr[1]) for attr in mock_attributes] - beamline_parameters.BEAMLINE_PARAMETER_PATHS[bl_name] = I04_BEAMLINE_PARAMETERS + [bl_module.__setattr__(attr, value) for attr, value in mock_attributes.items()] @pytest.fixture diff --git a/tests/devices/beamlines/i03/test_undulator_dcm.py b/tests/devices/beamlines/i03/test_undulator_dcm.py index f94093dc46e..28c47e01fa8 100644 --- a/tests/devices/beamlines/i03/test_undulator_dcm.py +++ b/tests/devices/beamlines/i03/test_undulator_dcm.py @@ -48,17 +48,17 @@ def flush_event_loop_on_finish(): @pytest.fixture(autouse=True) -def always_set_config_client(): - set_config_client(ConfigClient("test")) +def always_set_config_client(mock_config_client: ConfigClient): + set_config_client(mock_config_client) @pytest.fixture -async def fake_undulator_dcm() -> UndulatorDCM: +async def fake_undulator_dcm(mock_config_client: ConfigClient) -> UndulatorDCM: async with init_devices(mock=True): baton = Baton("BATON-01:") undulator = UndulatorInKeV( "UND-01", - ConfigClient(""), + mock_config_client, name="undulator", poles=80, id_gap_lookup_table_path=TEST_BEAMLINE_UNDULATOR_TO_GAP_LUT, @@ -70,7 +70,7 @@ async def fake_undulator_dcm() -> UndulatorDCM: undulator, dcm, daq_configuration_path=MOCK_DAQ_CONFIG_PATH, - config_client=ConfigClient(""), + config_client=mock_config_client, name="undulator_dcm", ) return undulator_dcm diff --git a/tests/devices/beamlines/i09_1_shared/conftest.py b/tests/devices/beamlines/i09_1_shared/conftest.py new file mode 100644 index 00000000000..3ec19a71153 --- /dev/null +++ b/tests/devices/beamlines/i09_1_shared/conftest.py @@ -0,0 +1,12 @@ +import pytest +from daq_config_server.models.lookup_tables.insertion_device import ( + parse_i09_hu_undulator_energy_gap_lut, +) + +from dodal.testing.fixtures.config_client import ConverterDict +from tests.devices.beamlines.i09_1_shared.test_data import TEST_HARD_UNDULATOR_LUT + + +@pytest.fixture +def mock_config_client_data() -> ConverterDict: + return {TEST_HARD_UNDULATOR_LUT: parse_i09_hu_undulator_energy_gap_lut} diff --git a/tests/devices/beamlines/i09_1_shared/test_hard_energy.py b/tests/devices/beamlines/i09_1_shared/test_hard_energy.py index fb4720e71c7..6df8bb0d510 100644 --- a/tests/devices/beamlines/i09_1_shared/test_hard_energy.py +++ b/tests/devices/beamlines/i09_1_shared/test_hard_energy.py @@ -5,10 +5,7 @@ from bluesky.run_engine import RunEngine from daq_config_server import ConfigClient from ophyd_async.core import init_devices -from ophyd_async.testing import ( - assert_reading, - partial_reading, -) +from ophyd_async.testing import assert_reading, partial_reading from dodal.devices.beamlines.i09_1_shared import ( HardEnergy, @@ -22,8 +19,7 @@ StationaryCrystal, ) from dodal.devices.undulator import UndulatorInMm, UndulatorOrder - -pytest_plugins = ["dodal.testing.fixtures.devices.hard_undulator"] +from tests.devices.beamlines.i09_1_shared.test_data import TEST_HARD_UNDULATOR_LUT @pytest.fixture @@ -51,7 +47,7 @@ async def undulator_in_mm() -> UndulatorInMm: @pytest.fixture async def hu_id_energy( - mock_config_client: ConfigClient, + mock_config_client_with_data: ConfigClient, undulator_order: UndulatorOrder, undulator_in_mm: UndulatorInMm, ) -> HardInsertionDeviceEnergy: @@ -59,8 +55,8 @@ async def hu_id_energy( hu_id_energy = HardInsertionDeviceEnergy( undulator_order=undulator_order, undulator=undulator_in_mm, - config_server=mock_config_client, - filepath="path/to/lut", + config_server=mock_config_client_with_data, + filepath=TEST_HARD_UNDULATOR_LUT, gap_to_energy_func=calculate_energy_i09_hu, energy_to_gap_func=calculate_gap_i09_hu, ) diff --git a/tests/devices/beamlines/i09_1_shared/test_undulator_functions.py b/tests/devices/beamlines/i09_1_shared/test_undulator_functions.py index 38766b32e4f..0bd400ab86b 100644 --- a/tests/devices/beamlines/i09_1_shared/test_undulator_functions.py +++ b/tests/devices/beamlines/i09_1_shared/test_undulator_functions.py @@ -4,25 +4,27 @@ import pytest from daq_config_server import ConfigClient from daq_config_server.models.lookup_tables import GenericLookupTable +from daq_config_server.models.lookup_tables.insertion_device import ( + parse_i09_hu_undulator_energy_gap_lut, +) from dodal.devices.beamlines.i09_1_shared import ( calculate_energy_i09_hu, calculate_gap_i09_hu, ) - -pytest_plugins = ["dodal.testing.fixtures.devices.hard_undulator"] +from tests.devices.beamlines.i09_1_shared.test_data import TEST_HARD_UNDULATOR_LUT @pytest.fixture() def lut( mock_config_client: ConfigClient, ) -> GenericLookupTable: - _lut = mock_config_client.get_file_contents( - file_path="path/to/lut", + return mock_config_client.get_file_contents( + file_path=TEST_HARD_UNDULATOR_LUT, desired_return_type=GenericLookupTable, reset_cached_result=True, + force_parser=parse_i09_hu_undulator_energy_gap_lut, ) - return _lut @pytest.mark.parametrize( diff --git a/tests/devices/beamlines/i09_2_shared/test_i09_apple2.py b/tests/devices/beamlines/i09_2_shared/test_i09_apple2.py index 1fc0592867d..215abeb5818 100644 --- a/tests/devices/beamlines/i09_2_shared/test_i09_apple2.py +++ b/tests/devices/beamlines/i09_2_shared/test_i09_apple2.py @@ -42,7 +42,7 @@ assert_expected_lut_file_equals_config_server_energy_motor_update_lookup_table, ) -# add mock_config_client, mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. +# mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. pytest_plugins = ["dodal.testing.fixtures.devices.apple2"] diff --git a/tests/devices/beamlines/i10/test_i10_apple2.py b/tests/devices/beamlines/i10/test_i10_apple2.py index 35a2465cb87..8d11a44cb92 100644 --- a/tests/devices/beamlines/i10/test_i10_apple2.py +++ b/tests/devices/beamlines/i10/test_i10_apple2.py @@ -57,7 +57,7 @@ assert_expected_lut_file_equals_config_server_energy_motor_update_lookup_table, ) -# add mock_config_client, mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. +# mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. pytest_plugins = ["dodal.testing.fixtures.devices.apple2"] diff --git a/tests/devices/beamlines/i15_1/test_blower.py b/tests/devices/beamlines/i15_1/test_blower.py index a67c1b3f869..18aaaa6326b 100644 --- a/tests/devices/beamlines/i15_1/test_blower.py +++ b/tests/devices/beamlines/i15_1/test_blower.py @@ -7,8 +7,10 @@ from tests.test_data import TEST_XPDF_LOCAL_PARAMETERS -def test_blower_config_client_reads_config_file_successfully(): - blower = Blower("", ConfigClient(""), TEST_XPDF_LOCAL_PARAMETERS) +def test_blower_config_client_reads_config_file_successfully( + mock_config_client: ConfigClient, +): + blower = Blower("", mock_config_client, TEST_XPDF_LOCAL_PARAMETERS) assert blower.get_config() == TemperatureControllerParams( beam_position=44.7, safe_position=2.0, diff --git a/tests/devices/beamlines/i15_1/test_cobra.py b/tests/devices/beamlines/i15_1/test_cobra.py index 3a647d93ceb..fcf7615ccc0 100644 --- a/tests/devices/beamlines/i15_1/test_cobra.py +++ b/tests/devices/beamlines/i15_1/test_cobra.py @@ -7,8 +7,10 @@ from tests.test_data import TEST_XPDF_LOCAL_PARAMETERS -def test_cobra_config_client_reads_config_file_successfully(): - cobra = Cobra("", ConfigClient(""), TEST_XPDF_LOCAL_PARAMETERS) +def test_cobra_config_client_reads_config_file_successfully( + mock_config_client: ConfigClient, +): + cobra = Cobra("", mock_config_client, TEST_XPDF_LOCAL_PARAMETERS) assert cobra.get_config() == TemperatureControllerParams( beam_position=461.5, safe_position=2.0, diff --git a/tests/devices/beamlines/i15_1/test_cryostream.py b/tests/devices/beamlines/i15_1/test_cryostream.py index 322631f6562..92be15683ef 100644 --- a/tests/devices/beamlines/i15_1/test_cryostream.py +++ b/tests/devices/beamlines/i15_1/test_cryostream.py @@ -7,8 +7,10 @@ from tests.test_data import TEST_XPDF_LOCAL_PARAMETERS -def test_cryostream_config_client_reads_config_file_successfully(): - cryostream = Cryostream("", ConfigClient(""), TEST_XPDF_LOCAL_PARAMETERS) +def test_cryostream_config_client_reads_config_file_successfully( + mock_config_client: ConfigClient, +): + cryostream = Cryostream("", mock_config_client, TEST_XPDF_LOCAL_PARAMETERS) assert cryostream.get_config() == TemperatureControllerParams( beam_position=469.9, safe_position=0.0, diff --git a/tests/devices/beamlines/i15_1/test_laue_monochrometer.py b/tests/devices/beamlines/i15_1/test_laue_monochrometer.py index d9ca2b91c10..91f04ec5a31 100644 --- a/tests/devices/beamlines/i15_1/test_laue_monochrometer.py +++ b/tests/devices/beamlines/i15_1/test_laue_monochrometer.py @@ -9,11 +9,11 @@ @pytest.fixture -def laue_monochrometer(): +def laue_monochrometer(mock_config_client: ConfigClient): with init_devices(mock=True): monocrometer = LaueMonochrometer( prefix="", - config_client=ConfigClient(""), + config_client=mock_config_client, crystal_lut_path=TEST_I15_1_CRYSTAL_LUT, ) return monocrometer diff --git a/tests/devices/beamlines/i17/test_i17_apple2.py b/tests/devices/beamlines/i17/test_i17_apple2.py index 40473b54d5d..b4814e152f8 100644 --- a/tests/devices/beamlines/i17/test_i17_apple2.py +++ b/tests/devices/beamlines/i17/test_i17_apple2.py @@ -14,7 +14,7 @@ ) from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup -# add mock_config_client, mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. +# mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. pytest_plugins = ["dodal.testing.fixtures.devices.apple2"] diff --git a/tests/devices/beamlines/i24/test_vgonio.py b/tests/devices/beamlines/i24/test_vgonio.py index 891181444bb..d5be6ee5c0e 100644 --- a/tests/devices/beamlines/i24/test_vgonio.py +++ b/tests/devices/beamlines/i24/test_vgonio.py @@ -1,14 +1,15 @@ import bluesky.plan_stubs as bps import pytest from bluesky import RunEngine +from ophyd_async.core import init_devices from dodal.devices.beamlines.i24.vgonio import VerticalGoniometer @pytest.fixture -async def vgonio(): - vgonio = VerticalGoniometer("", name="fake_vgonio") - await vgonio.connect(mock=True) +def vgonio() -> VerticalGoniometer: + with init_devices(mock=True): + vgonio = VerticalGoniometer("") return vgonio @@ -22,3 +23,8 @@ async def test_vgonio_motor_move(vgonio: VerticalGoniometer, run_engine: RunEngi assert await vgonio.x.user_readback.get_value() == 1.0 assert await vgonio.yh.user_readback.get_value() == 1.5 + + +def test_device_creation_names(vgonio: VerticalGoniometer): + assert vgonio._name == "vgonio" + assert vgonio.omega._name == "vgonio-omega" diff --git a/tests/devices/detector/test_det_resolution.py b/tests/devices/detector/test_det_resolution.py index b8b75dcea3e..0ce1e5e983b 100644 --- a/tests/devices/detector/test_det_resolution.py +++ b/tests/devices/detector/test_det_resolution.py @@ -17,8 +17,8 @@ @pytest.fixture(autouse=True) -def always_set_config_client(): - set_config_client(ConfigClient("test")) +def always_set_config_client(mock_config_client: ConfigClient): + set_config_client(mock_config_client) @pytest.fixture(scope="function") diff --git a/tests/devices/detector/test_detector.py b/tests/devices/detector/test_detector.py index 80b071aa8a1..653dc7550c1 100644 --- a/tests/devices/detector/test_detector.py +++ b/tests/devices/detector/test_detector.py @@ -4,6 +4,7 @@ import pytest from pydantic import ValidationError +from dodal.common.beamlines.beamline_utils import set_config_client from dodal.devices.detector import DetectorParams from dodal.devices.detector.det_dim_constants import EIGER2_X_16M_SIZE from tests.devices.test_data import TEST_LUT_TXT @@ -45,15 +46,15 @@ def test_if_path_provided_check_is_dir(tmp_path: Path): create_det_params_with_dir_and_prefix(file_path) -@patch( - "dodal.devices.detector.detector.get_config_client", -) +@pytest.fixture(autouse=True) +def always_set_config_client(): + set_config_client(MagicMock()) + + @patch( "dodal.devices.detector.det_dist_to_beam_converter.linear_extrapolation_lut", ) -def test_correct_det_dist_to_beam_converter_path_passed_in( - mock_lut, mock_get_config_client, tmp_path -): +def test_correct_det_dist_to_beam_converter_path_passed_in(mock_lut, tmp_path): params = DetectorParams( expected_energy_ev=100, exposure_time_s=1.0, diff --git a/tests/devices/insertion_device/test_apple_knot_path_finder.py b/tests/devices/insertion_device/test_apple_knot_path_finder.py index 07ebb35c70d..bd33f79b509 100644 --- a/tests/devices/insertion_device/test_apple_knot_path_finder.py +++ b/tests/devices/insertion_device/test_apple_knot_path_finder.py @@ -8,7 +8,7 @@ Apple2Val, ) -# add mock_config_client, mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. +# mock_id_gap, mock_phase and mock_jaw_phase_axes to pytest. pytest_plugins = [ "dodal.testing.fixtures.devices.apple2", "dodal.testing.fixtures.devices.apple_knot", diff --git a/tests/devices/mx_phase1/test_beamstop.py b/tests/devices/mx_phase1/test_beamstop.py index 599a763678c..83f276c4ef0 100644 --- a/tests/devices/mx_phase1/test_beamstop.py +++ b/tests/devices/mx_phase1/test_beamstop.py @@ -7,16 +7,16 @@ from bluesky import plan_stubs as bps from bluesky.preprocessors import run_decorator from bluesky.run_engine import RunEngine +from daq_config_server import ConfigClient from ophyd_async.core import get_mock, get_mock_put, set_mock_value from dodal.devices.beamlines.i03 import Beamstop, BeamstopPositions -from dodal.testing.fixtures.config_server import fake_config_server_get_file_contents from tests.common.beamlines.test_beamline_parameters import TEST_BEAMLINE_PARAMETERS_TXT @pytest.fixture -def beamline_parameters() -> dict[str, Any]: - return fake_config_server_get_file_contents(TEST_BEAMLINE_PARAMETERS_TXT, dict) +def beamline_parameters(mock_config_client: ConfigClient) -> dict[str, Any]: + return mock_config_client.get_file_contents(TEST_BEAMLINE_PARAMETERS_TXT, dict) @pytest.mark.parametrize( diff --git a/tests/devices/oav/conftest.py b/tests/devices/oav/conftest.py index 8da3a5512f5..78b1dc7617b 100644 --- a/tests/devices/oav/conftest.py +++ b/tests/devices/oav/conftest.py @@ -10,9 +10,9 @@ @pytest.fixture -async def oav() -> OAVBeamCentreFile: +async def oav(mock_config_client: ConfigClient) -> OAVBeamCentreFile: oav_config = OAVConfigBeamCentre( - TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, ConfigClient("") + TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, mock_config_client ) async with init_devices(mock=True, connect=True): oav = OAVBeamCentreFile("", config=oav_config, name="oav") @@ -27,8 +27,8 @@ async def oav() -> OAVBeamCentreFile: @pytest.fixture -async def oav_beam_centre_pv_roi() -> OAVBeamCentrePV: - oav_config = OAVConfig(TEST_OAV_ZOOM_LEVELS, ConfigClient("")) +async def oav_beam_centre_pv_roi(mock_config_client: ConfigClient) -> OAVBeamCentrePV: + oav_config = OAVConfig(TEST_OAV_ZOOM_LEVELS, mock_config_client) async with init_devices(mock=True, connect=True): oav = OAVBeamCentrePV("", config=oav_config, name="oav") zoom_levels_list = ["1.0x", "3.0x", "5.0x", "7.5x", "10.0x"] @@ -42,8 +42,8 @@ async def oav_beam_centre_pv_roi() -> OAVBeamCentrePV: @pytest.fixture -async def oav_beam_centre_pv_fs() -> OAVBeamCentrePV: - oav_config = OAVConfig(TEST_OAV_ZOOM_LEVELS, ConfigClient("")) +async def oav_beam_centre_pv_fs(mock_config_client: ConfigClient) -> OAVBeamCentrePV: + oav_config = OAVConfig(TEST_OAV_ZOOM_LEVELS, mock_config_client) async with init_devices(mock=True, connect=True): oav = OAVBeamCentrePV( "", config=oav_config, name="oav", mjpeg_prefix="XTAL", overlay_channel=3 diff --git a/tests/devices/oav/test_oav.py b/tests/devices/oav/test_oav.py index 5c4987e2c63..cb0c525fe1d 100644 --- a/tests/devices/oav/test_oav.py +++ b/tests/devices/oav/test_oav.py @@ -164,9 +164,11 @@ async def test_beam_centre_signals_have_same_names( assert "oav-beam_centre_j" in reading.keys() -async def test_oav_with_null_zoom_controller(null_controller: NullZoomController): +async def test_oav_with_null_zoom_controller( + null_controller: NullZoomController, mock_config_client: ConfigClient +): oav_config = OAVConfigBeamCentre( - TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, ConfigClient("") + TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, mock_config_client ) oav = OAVBeamCentreFile("", oav_config, "", zoom_controller=null_controller) @@ -192,9 +194,11 @@ async def test_oav_with_null_zoom_controller_set_zoom_level_other_than_1( "mjpeg_prefix", ["MJPG", "XTAL"], ) -async def test_setting_mjpeg_prefix_changes_stream_url(mjpeg_prefix): +async def test_setting_mjpeg_prefix_changes_stream_url( + mjpeg_prefix, mock_config_client: ConfigClient +): oav_config = OAVConfigBeamCentre( - TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, ConfigClient("") + TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, mock_config_client ) async with init_devices(mock=True, connect=True): oav = OAVBeamCentreFile( diff --git a/tests/devices/oav/test_oav_parameters.py b/tests/devices/oav/test_oav_parameters.py index a56419c7b12..ded3c7c30ea 100644 --- a/tests/devices/oav/test_oav_parameters.py +++ b/tests/devices/oav/test_oav_parameters.py @@ -18,19 +18,21 @@ @pytest.fixture -def mock_parameters(): - return OAVParameters(ConfigClient(""), "loopCentring", TEST_OAV_CENTRING_JSON) +def mock_parameters(mock_config_client: ConfigClient): + return OAVParameters(mock_config_client, "loopCentring", TEST_OAV_CENTRING_JSON) @pytest.fixture -def mock_config() -> dict[str, ZoomParams]: - return OAVConfig(TEST_OAV_ZOOM_LEVELS, ConfigClient("")).get_parameters() +def mock_config(mock_config_client: ConfigClient) -> dict[str, ZoomParams]: + return OAVConfig(TEST_OAV_ZOOM_LEVELS, mock_config_client).get_parameters() @pytest.fixture -def mock_config_with_beam_centre() -> dict[str, ZoomParamsCrosshair]: +def mock_config_with_beam_centre( + mock_config_client: ConfigClient, +) -> dict[str, ZoomParamsCrosshair]: config = OAVConfigBeamCentre( - TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, ConfigClient("") + TEST_OAV_ZOOM_LEVELS, TEST_DISPLAY_CONFIG, mock_config_client ).get_parameters() config = cast(dict[str, ZoomParamsCrosshair], config) return config diff --git a/tests/devices/test_beam_converter.py b/tests/devices/test_beam_converter.py index 4bdbe687ce9..33c92862fab 100644 --- a/tests/devices/test_beam_converter.py +++ b/tests/devices/test_beam_converter.py @@ -17,12 +17,12 @@ @pytest.fixture -def fake_converter(tmp_path): +def fake_converter(tmp_path, mock_config_client: ConfigClient): lookup_table_path = tmp_path / "test.txt" with open(lookup_table_path, "w") as f: f.write(LOOKUP_TABLE_TEST_VALUES.model_dump_json()) - yield DetectorDistanceToBeamXYConverter(lookup_table_path, ConfigClient("")) + yield DetectorDistanceToBeamXYConverter(lookup_table_path, mock_config_client) @pytest.mark.parametrize( diff --git a/tests/devices/test_eiger.py b/tests/devices/test_eiger.py index e42dd11f9d3..95a8d47fb79 100644 --- a/tests/devices/test_eiger.py +++ b/tests/devices/test_eiger.py @@ -32,8 +32,8 @@ class StatusError(Exception): @pytest.fixture(autouse=True) -def always_set_config_client(): - set_config_client(ConfigClient("test")) +def always_set_config_client(mock_config_client: ConfigClient): + set_config_client(mock_config_client) @pytest.fixture diff --git a/tests/devices/test_focusing_mirror.py b/tests/devices/test_focusing_mirror.py index feb79c6f21c..91d75f4316f 100644 --- a/tests/devices/test_focusing_mirror.py +++ b/tests/devices/test_focusing_mirror.py @@ -236,13 +236,13 @@ def test_mirror_set_voltage_returns_immediately_if_voltage_already_demanded( get_mock_put(mirror_voltage_with_set._setpoint_v).assert_not_called() -def test_mirror_populates_voltage_channels(): +def test_mirror_populates_voltage_channels(mock_config_client: ConfigClient): with init_devices(mock=True): mirror_voltages = MirrorVoltages( "", "", daq_configuration_path=MOCK_DAQ_CONFIG_PATH, - config_client=ConfigClient(""), + config_client=mock_config_client, ) assert len(mirror_voltages.horizontal_voltages) == 14 assert len(mirror_voltages.vertical_voltages) == 8 diff --git a/tests/devices/test_undulator.py b/tests/devices/test_undulator.py index c04e1fd4317..b925d5b0d19 100644 --- a/tests/devices/test_undulator.py +++ b/tests/devices/test_undulator.py @@ -33,12 +33,12 @@ @pytest.fixture -async def undulator() -> UndulatorInKeV: +async def undulator(mock_config_client: ConfigClient) -> UndulatorInKeV: async with init_devices(mock=True): baton = Baton("BATON-01") undulator = UndulatorInKeV( "UND-01", - ConfigClient(""), + mock_config_client, name="undulator", poles=80, length=2.0, @@ -113,11 +113,11 @@ async def test_configuration_includes_configuration_fields(undulator: UndulatorI ) -async def test_poles_not_propagated_if_not_supplied(): +async def test_poles_not_propagated_if_not_supplied(mock_config_client: ConfigClient): async with init_devices(mock=True): undulator = UndulatorInKeV( "UND-01", - ConfigClient(""), + mock_config_client, name="undulator", length=2.0, id_gap_lookup_table_path=TEST_BEAMLINE_UNDULATOR_TO_GAP_LUT, @@ -126,11 +126,11 @@ async def test_poles_not_propagated_if_not_supplied(): assert "undulator-poles" not in (await undulator.read_configuration()) -async def test_length_not_propagated_if_not_supplied(): +async def test_length_not_propagated_if_not_supplied(mock_config_client: ConfigClient): async with init_devices(mock=True): undulator = UndulatorInKeV( "UND-01", - ConfigClient(""), + mock_config_client, name="undulator", poles=80, id_gap_lookup_table_path=TEST_BEAMLINE_UNDULATOR_TO_GAP_LUT, @@ -151,7 +151,7 @@ def test_correct_closest_distance_to_energy_from_table(energy, expected_output): async def test_when_gap_access_is_disabled_set_then_error_is_raised( - undulator, + undulator: UndulatorInKeV, ): set_mock_value(undulator.gap_access, EnabledDisabledUpper.DISABLED) with pytest.raises(AccessError): diff --git a/tests/devices/util/test_lookup_tables.py b/tests/devices/util/test_lookup_tables.py index 55313c0b575..5e00e8125ad 100644 --- a/tests/devices/util/test_lookup_tables.py +++ b/tests/devices/util/test_lookup_tables.py @@ -21,10 +21,11 @@ ) -async def test_energy_to_distance_table_correct_format(): - config_server = ConfigClient("") +async def test_energy_to_distance_table_correct_format( + mock_config_client: ConfigClient, +): table = np.array( - config_server.get_file_contents( + mock_config_client.get_file_contents( TEST_BEAMLINE_UNDULATOR_TO_GAP_LUT, UndulatorEnergyGapLookupTable ).rows ) diff --git a/tests/plan_stubs/test_topup_plan.py b/tests/plan_stubs/test_topup_plan.py index 7f845b84cea..007dbab2701 100644 --- a/tests/plan_stubs/test_topup_plan.py +++ b/tests/plan_stubs/test_topup_plan.py @@ -20,8 +20,8 @@ @pytest.fixture(autouse=True) -def always_set_config_client(): - set_config_client(ConfigClient("test")) +def always_set_config_client(mock_config_client: ConfigClient): + set_config_client(mock_config_client) @pytest.fixture diff --git a/tests/plans/conftest.py b/tests/plans/conftest.py index ac002e729e5..bfb6d2f1f47 100644 --- a/tests/plans/conftest.py +++ b/tests/plans/conftest.py @@ -26,11 +26,13 @@ def __init__(self, undulator: UndulatorInKeV, dcm: DoubleCrystalMonochromatorBas @pytest.fixture -async def mock_undulator_and_dcm() -> UndulatorGapCheckDevices: +async def mock_undulator_and_dcm( + mock_config_client: ConfigClient, +) -> UndulatorGapCheckDevices: async with init_devices(mock=True): undulator = UndulatorInKeV( "", - ConfigClient(""), + mock_config_client, id_gap_lookup_table_path=TEST_BEAMLINE_UNDULATOR_TO_GAP_LUT, ) dcm = DCM("") diff --git a/tests/plans/device_setup_plans/test_setup_pin_tip.py b/tests/plans/device_setup_plans/test_setup_pin_tip.py index e94192d10a6..718febe3e68 100644 --- a/tests/plans/device_setup_plans/test_setup_pin_tip.py +++ b/tests/plans/device_setup_plans/test_setup_pin_tip.py @@ -20,8 +20,8 @@ def pin_tip_detection() -> PinTipDetection: @pytest.fixture -def params() -> OAVParameters: - return OAVParameters(ConfigClient(""), "pinTipCentring", TEST_OAV_CENTRING_JSON) +def params(mock_config_client: ConfigClient) -> OAVParameters: + return OAVParameters(mock_config_client, "pinTipCentring", TEST_OAV_CENTRING_JSON) @pytest.mark.parametrize( diff --git a/tests/plans/test_configure_arm_trigger_and_disarm_detector.py b/tests/plans/test_configure_arm_trigger_and_disarm_detector.py index 17c8fdd6f0a..6a859053e28 100644 --- a/tests/plans/test_configure_arm_trigger_and_disarm_detector.py +++ b/tests/plans/test_configure_arm_trigger_and_disarm_detector.py @@ -28,9 +28,12 @@ async def fake_eiger() -> FastEiger: async def test_configure_arm_trigger_and_disarm_detector( - fake_eiger: FastEiger, eiger_params: DetectorParams, run_engine: RunEngine + fake_eiger: FastEiger, + eiger_params: DetectorParams, + run_engine: RunEngine, + mock_config_client: ConfigClient, ): - set_config_client(ConfigClient("test")) + set_config_client(mock_config_client) trigger_info = TriggerInfo( # Manual trigger, so setting number of triggers to 1. number_of_events=1,