From 7cda787a01df58daafbfd021ff170856c7987918 Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Tue, 4 Jun 2024 09:03:48 -0700 Subject: [PATCH 1/6] BLD: add caproto and deps to dev requirements --- conda-recipe/meta.yaml | 2 ++ dev-requirements.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index f07ee2c..51629c1 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -33,7 +33,9 @@ test: imports: - {{ import_name }} requires: + - caproto - coverage + - numpy - pytest - pytest-asyncio - pytest-qt diff --git a/dev-requirements.txt b/dev-requirements.txt index 6d998b6..5f7c2c4 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,6 +1,8 @@ # These are required for developing the package (running the tests) but not # necessarily required for _using_ it. +caproto coverage +numpy pytest pytest-asyncio pytest-cov From c526e908f7f6e4b1dd9cdcdc30f3460b0e86c4e5 Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Tue, 4 Jun 2024 10:25:26 -0700 Subject: [PATCH 2/6] TST: implement IOCFactory and TempIOC TempIOC can be used as a context manager to make PVs accessible via EPICS during tests. IOCFactory uses Entry trees to define and instantiate TempIOCs, so that tests can choose which PVs they need to be available. --- superscore/tests/ioc/__init__.py | 1 + superscore/tests/ioc/ioc_factory.py | 71 +++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 superscore/tests/ioc/__init__.py create mode 100644 superscore/tests/ioc/ioc_factory.py diff --git a/superscore/tests/ioc/__init__.py b/superscore/tests/ioc/__init__.py new file mode 100644 index 0000000..a5e5cbd --- /dev/null +++ b/superscore/tests/ioc/__init__.py @@ -0,0 +1 @@ +from .ioc_factory import IOCFactory # noqa: F401 diff --git a/superscore/tests/ioc/ioc_factory.py b/superscore/tests/ioc/ioc_factory.py new file mode 100644 index 0000000..f969022 --- /dev/null +++ b/superscore/tests/ioc/ioc_factory.py @@ -0,0 +1,71 @@ +from multiprocessing import Process +from typing import Iterable, Mapping, Union + +from caproto.server import PVGroup, pvproperty +from caproto.server import run as run_ioc +from epicscorelibs.ca import dbr + +from superscore.model import Entry, Nestable, Parameter, Readback, Setpoint + + +class TempIOC(PVGroup): + """ + Makes PVs accessible via EPICS when running. Instances automatically start + and stop running when used as a context manager, and are thus suitable for + use in tests. + """ + def __enter__(self): + self.running_process = Process( + target=run_ioc, + args=(self.pvdb,), + daemon=True, + ) + self.running_process.start() + return self + + def __exit__(self, exc_type, exc_value, traceback): + pass + + +class IOCFactory: + """ + Generates TempIOC subclasses bound to a set of PVs. + """ + @staticmethod + def from_entries(entries: Iterable[Entry], **ioc_options) -> PVGroup: + """ + Defines and instantiates a TempIOC subclass containing all PVs reachable + from entries. + """ + attrs = IOCFactory.prepare_attrs(entries) + IOC = type("IOC", (TempIOC,), attrs) + return IOC + + @staticmethod + def collect_pvs(entries: Iterable[Entry]) -> Iterable[Union[Parameter, Setpoint, Readback]]: + """Returns a collection of all PVs reachable from entries""" + pvs = [] + q = entries.copy() + while len(q) > 0: + entry = q.pop() + if isinstance(entry, Nestable): + q.extend(entry.children) + else: + pvs.append(entry) + return pvs + + @staticmethod + def prepare_attrs(entries: Iterable[Entry]) -> Mapping[str, pvproperty]: + """ + Turns a collecton of PVs into a Mapping from attribute names to + caproto.pvproperties. The mapping is suitable for passing into a type() + call as the dict arg. + """ + pvs = IOCFactory.collect_pvs(entries) + attrs = {} + for entry in pvs: + value = entry.data if isinstance(entry, (Setpoint, Readback)) else None + pv = pvproperty(name=entry.pv_name, doc=entry.description, value=value, dtype=dbr.DBR_STRING if isinstance(entry.data, str) else None) + attr = "".join([c.lower() for c in entry.pv_name if c != ':']) + attrs[attr] = pv + return attrs From 6166def4dd0bcc451e421cbe5a52b335a211553a Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Thu, 6 Jun 2024 12:01:27 -0700 Subject: [PATCH 3/6] TST: add IOC fixture mirroring LINAC model --- superscore/tests/conftest.py | 17 +++++++++++++++-- superscore/tests/test_ioc.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 superscore/tests/test_ioc.py diff --git a/superscore/tests/conftest.py b/superscore/tests/conftest.py index b465838..1f1292b 100644 --- a/superscore/tests/conftest.py +++ b/superscore/tests/conftest.py @@ -13,10 +13,10 @@ from superscore.control_layers.core import ControlLayer from superscore.model import (Collection, Parameter, Readback, Root, Setpoint, Snapshot) +from superscore.tests.ioc import IOCFactory -@pytest.fixture(scope='function') -def linac_backend(): +def linac_data(): lasr_gunb_pv1 = Parameter( uuid="5544c58f-88b6-40aa-9076-f180a44908f5", pv_name="LASR:GUNB:TEST1", @@ -615,6 +615,12 @@ def linac_backend(): ] ) + return all_col, all_snapshot + + +@pytest.fixture(scope='function') +def linac_backend(): + all_col, all_snapshot = linac_data() return TestBackend([all_col, all_snapshot]) @@ -772,3 +778,10 @@ def sample_client( client.cl = dummy_cl return client + + +@pytest.fixture(scope='module') +def linac_ioc(): + _, snapshot = linac_data() + with IOCFactory.from_entries(snapshot.children)(prefix="SCORETEST:") as ioc: + yield ioc diff --git a/superscore/tests/test_ioc.py b/superscore/tests/test_ioc.py new file mode 100644 index 0000000..7d0382b --- /dev/null +++ b/superscore/tests/test_ioc.py @@ -0,0 +1,20 @@ +from superscore.control_layers.core import ControlLayer + + +def test_ioc(linac_ioc): + cl = ControlLayer() + assert cl.get("SCORETEST:MGNT:GUNB:TEST0").data == 1 + cl.put("SCORETEST:MGNT:GUNB:TEST0", 0) + assert cl.get("SCORETEST:MGNT:GUNB:TEST0").data == 0 + + assert cl.get("SCORETEST:VAC:GUNB:TEST1").data == "Ion Pump" + cl.put("SCORETEST:VAC:GUNB:TEST1", "new value") + assert cl.get("SCORETEST:VAC:GUNB:TEST1").data == "new value" + + assert cl.get("SCORETEST:LASR:GUNB:TEST2").data == 5 + cl.put("SCORETEST:LASR:GUNB:TEST2", 10) + assert cl.get("SCORETEST:LASR:GUNB:TEST2").data == 10 + + assert cl.get("SCORETEST:LASR:IN10:TEST0").data == 645.26 + cl.put("SCORETEST:LASR:IN10:TEST0", 600.0) + assert cl.get("SCORETEST:LASR:IN10:TEST0").data == 600.0 From f289aeef251c7c68742d0dfb3964dfa9f7768c10 Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Fri, 16 Aug 2024 10:55:08 -0700 Subject: [PATCH 4/6] TST: add IOC module file --- superscore/tests/ioc/linac.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 superscore/tests/ioc/linac.py diff --git a/superscore/tests/ioc/linac.py b/superscore/tests/ioc/linac.py new file mode 100644 index 0000000..58ba634 --- /dev/null +++ b/superscore/tests/ioc/linac.py @@ -0,0 +1,15 @@ +from caproto.server import ioc_arg_parser, run + +from superscore.tests.conftest import linac_data +from superscore.tests.ioc import IOCFactory + +if __name__ == '__main__': + _, snapshot = linac_data() + LinacIOC = IOCFactory.from_entries(snapshot.children) + + ioc_options, run_options = ioc_arg_parser( + default_prefix='SCORETEST:', + desc="IOC evoking the structure of a linac", + ) + ioc = LinacIOC(**ioc_options) + run(ioc.pvdb, **run_options) From 5f5d5bcc7580fe18160143e081cbcd15b34e4940 Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Mon, 19 Aug 2024 10:50:20 -0700 Subject: [PATCH 5/6] BUG: fix dummy ControlLayer shims side-effect Since ControlLayer sets its .shims to SHIMS by default, setting attributes in dummy cl .shims changes SHIMS for the rest of the test session. Creating a new dict for the dummy_shim fixture avoids this side-effect. For redundancy, I also changed ControlLayer.__init__ to copy SHIMS instead of using the global instance. --- superscore/control_layers/core.py | 2 +- superscore/tests/conftest.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/superscore/control_layers/core.py b/superscore/control_layers/core.py index c0f3a19..b77c88a 100644 --- a/superscore/control_layers/core.py +++ b/superscore/control_layers/core.py @@ -32,7 +32,7 @@ class ControlLayer: def __init__(self, *args, shims: Optional[List[str]] = None, **kwargs): if shims is None: # load all available shims - self.shims = SHIMS + self.shims = SHIMS.copy() logger.debug('No shims specified, loading all available communication ' f'shims: {list(self.shims.keys())}') else: diff --git a/superscore/tests/conftest.py b/superscore/tests/conftest.py index 1f1292b..689dd8f 100644 --- a/superscore/tests/conftest.py +++ b/superscore/tests/conftest.py @@ -742,8 +742,7 @@ def monitor(self, *args, **kwargs): @pytest.fixture(scope='function') def dummy_cl() -> ControlLayer: cl = ControlLayer() - cl.shims['ca'] = DummyShim() - cl.shims['pva'] = DummyShim() + cl.shims = {protocol: DummyShim() for protocol in ['ca', 'pva']} return cl From f06d2e5035dd2e4fd468c95ca88088e2cd95d9f9 Mon Sep 17 00:00:00 2001 From: Devan Agrawal Date: Mon, 19 Aug 2024 15:11:00 -0700 Subject: [PATCH 6/6] BLD: pre-release notes --- .../34-ioc_infrastructure.rst | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/source/upcoming_release_notes/34-ioc_infrastructure.rst diff --git a/docs/source/upcoming_release_notes/34-ioc_infrastructure.rst b/docs/source/upcoming_release_notes/34-ioc_infrastructure.rst new file mode 100644 index 0000000..1995801 --- /dev/null +++ b/docs/source/upcoming_release_notes/34-ioc_infrastructure.rst @@ -0,0 +1,23 @@ +34 ioc infrastructure +################# + +API Breaks +---------- +- N/A + +Features +-------- +- implement fixture for running IOCs that can be queried for integration tests +- implement module that can run IOCs for demos + +Bugfixes +-------- +- don't change control_layer/core.py:SHIMS when creating dummy CLs + +Maintenance +----------- +- define linac testing structure outside of fixture + +Contributors +------------ +- shilorigins