Skip to content

Commit

Permalink
Add tests for EigerHandler and EigerConfigHandler (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
jsouter authored Dec 3, 2024
1 parent e9769a4 commit 3bdfc71
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 224 deletions.
6 changes: 4 additions & 2 deletions src/fastcs_eiger/eiger_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class EigerConfigHandler(EigerHandler):

first_poll_complete: bool = False

async def update(self, controller: "EigerController", attr: AttrR) -> None:
async def update(self, controller: "EigerSubsystemController", attr: AttrR) -> None:
# Only poll once on startup
if not self.first_poll_complete:
await super().update(controller, attr)
Expand All @@ -109,7 +109,9 @@ async def update(self, controller: "EigerController", attr: AttrR) -> None:

self.first_poll_complete = True

async def config_update(self, controller: "EigerController", attr: AttrR) -> None:
async def config_update(
self, controller: "EigerSubsystemController", attr: AttrR
) -> None:
await super().update(controller, attr)


Expand Down
155 changes: 29 additions & 126 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import os
import signal
import subprocess
from time import sleep
from typing import Any

import pytest
from pytest_mock import MockerFixture

from fastcs_eiger.eiger_controller import EigerController

# Prevent pytest from catching exceptions when debugging in vscode so that break on
# exception works correctly (see: https://github.com/pytest-dev/pytest/issues/7409)
Expand All @@ -22,129 +26,28 @@ def pytest_internalerror(excinfo: pytest.ExceptionInfo[Any]):
raise excinfo.value


_detector_config_keys = [
"auto_summation",
"beam_center_x",
"beam_center_y",
"bit_depth_image",
"bit_depth_readout",
"chi_increment",
"chi_start",
"compression",
"count_time",
"counting_mode",
"countrate_correction_applied",
"countrate_correction_count_cutoff",
"data_collection_date",
"description",
"detector_distance",
"detector_number",
"detector_readout_time",
"eiger_fw_version",
"element",
"extg_mode",
"fast_arm",
"flatfield_correction_applied",
"frame_count_time",
"frame_time",
"incident_energy",
"incident_particle_type",
"instrument_name",
"kappa_increment",
"kappa_start",
"mask_to_zero",
"nexpi",
"nimages",
"ntrigger",
"ntriggers_skipped",
"number_of_excluded_pixels",
"omega_increment",
"omega_start",
"phi_increment",
"phi_start",
"photon_energy",
"pixel_mask_applied",
"roi_mode",
"sample_name",
"sensor_material",
"sensor_thickness",
"software_version",
"source_name",
"threshold/1/energy",
"threshold/1/mode",
"threshold/1/number_of_excluded_pixels",
"threshold/2/energy",
"threshold/2/mode",
"threshold/2/number_of_excluded_pixels",
"threshold/difference/lower_threshold",
"threshold/difference/mode",
"threshold/difference/upper_threshold",
"threshold_energy",
"total_flux",
"trigger_mode",
"trigger_start_delay",
"two_theta_increment",
"two_theta_start",
"virtual_pixel_correction_applied",
"x_pixel_size",
"x_pixels_in_detector",
"y_pixel_size",
"y_pixels_in_detector",
]

_detector_status_keys = [
"humidity",
"link_0",
"link_1",
"series_unique_id",
"state",
"temperature",
"time",
]

_stream_config_keys = [
"format",
"header_appendix",
"header_detail",
"image_appendix",
"mode",
]
_stream_status_keys = ["dropped", "state"]
_monitor_config_keys = ["buffer_size", "discard_new", "mode"]
_monitor_status_keys = ["buffer_free", "dropped", "error", "state"]


@pytest.fixture
def detector_config_keys():
return _detector_config_keys


@pytest.fixture
def detector_status_keys():
return _detector_status_keys


# Stolen from tickit-devices
# https://docs.pytest.org/en/latest/example/parametrize.html#indirect-parametrization
@pytest.fixture
def mock_connection(mocker: MockerFixture):
connection = mocker.patch("fastcs_eiger.http_connection.HTTPConnection")
connection.get = mocker.AsyncMock()

async def _connection_get(uri):
if "detector/api/1.8.0/status/keys" in uri:
return _detector_status_keys
elif "detector/api/1.8.0/config/keys" in uri:
return _detector_config_keys
elif "monitor/api/1.8.0/status/keys" in uri:
return _monitor_status_keys
elif "monitor/api/1.8.0/config/keys" in uri:
return _monitor_config_keys
elif "stream/api/1.8.0/status/keys" in uri:
return _stream_status_keys
elif "stream/api/1.8.0/config/keys" in uri:
return _stream_config_keys
else:
# dummy response
return {"access_mode": "rw", "value": 0.0, "value_type": "float"}

connection.get.side_effect = _connection_get
return connection
def sim_eiger_controller(request):
"""Subprocess that runs ``tickit all <config_path>``."""
config_path: str = request.param
proc = subprocess.Popen(
["tickit", "all", config_path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)

# Wait until ready
while True:
line = proc.stdout.readline()
if "Starting HTTP server..." in line:
break

sleep(3)

yield EigerController("127.0.0.1", 8081)

proc.send_signal(signal.SIGINT)
print(proc.communicate()[0])
91 changes: 54 additions & 37 deletions tests/system/test_introspection.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import json
import os
import signal
import subprocess
from pathlib import Path
from time import sleep
from typing import Any

import pytest
from fastcs.attributes import AttrR
from fastcs.attributes import Attribute, AttrR
from fastcs.datatypes import Float

from fastcs_eiger.eiger_controller import (
IGNORED_KEYS,
MISSING_KEYS,
EigerController,
EigerDetectorController,
EigerMonitorController,
Expand All @@ -34,43 +33,11 @@ def _serialise_parameter(parameter: EigerParameter) -> dict:
}


@pytest.fixture
def eiger_controller():
yield EigerController("i04-1-eiger01", 80)


# Stolen from tickit-devices
# https://docs.pytest.org/en/latest/example/parametrize.html#indirect-parametrization
@pytest.fixture
def sim_eiger_controller(request):
"""Subprocess that runs ``tickit all <config_path>``."""
config_path: str = request.param
proc = subprocess.Popen(
["tickit", "all", config_path],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)

# Wait until ready
while True:
line = proc.stdout.readline()
if "Starting HTTP server..." in line:
break

sleep(3)

yield EigerController("127.0.0.1", 8081)

proc.send_signal(signal.SIGINT)
print(proc.communicate()[0])


@pytest.mark.asyncio
@pytest.mark.parametrize(
"sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True
)
async def test_introspection(sim_eiger_controller: EigerController):
async def test_attribute_creation(sim_eiger_controller: EigerController):
controller = sim_eiger_controller
await controller.initialise()
serialised_parameters: dict[str, dict[str, Any]] = {}
Expand Down Expand Up @@ -116,3 +83,53 @@ async def test_introspection(sim_eiger_controller: EigerController):
)

await controller.connection.close()


@pytest.mark.asyncio
@pytest.mark.parametrize(
"sim_eiger_controller", [str(HERE / "eiger.yaml")], indirect=True
)
async def test_controller_groups_and_parameters(sim_eiger_controller: EigerController):
controller = sim_eiger_controller
await controller.initialise()

for subsystem in MISSING_KEYS:
subcontroller = controller.get_sub_controllers()[subsystem.title()]
parameters = await subcontroller._introspect_detector_subsystem()
if subsystem == "detector":
# ignored keys should not get added to the controller
assert all(param.key not in IGNORED_KEYS for param in parameters)

# threshold parameters should belong to own group
for attr_name in dir(subcontroller):
attr = getattr(subcontroller, attr_name)
if isinstance(attr, Attribute) and "threshold" in attr_name:
if attr_name == "threshold_energy":
continue
assert attr.group and "Threshold" in attr.group

attr = subcontroller.threshold_1_energy
sender = attr.sender
await sender.put(subcontroller, attr, 100.0)
# set parameters to update based on response to put request
assert subcontroller._parameter_updates == {
"flatfield",
"threshold/1/energy",
"threshold/1/flatfield",
"threshold/2/flatfield",
"threshold_energy",
}

subcontroller._parameter_updates.clear()

# make sure API inconsistency for threshold/difference/mode is addressed
attr = subcontroller.threshold_difference_mode
sender = attr.sender
await sender.put(subcontroller, attr, "enabled")
assert subcontroller._parameter_updates == {"threshold/difference/mode"}

for keys in MISSING_KEYS[subsystem].values(): # loop over status, config keys
for key in keys:
assert any(param.key == key for param in parameters)

await controller.connection.close()
Loading

0 comments on commit 3bdfc71

Please sign in to comment.