diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..be643311 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "example/t01-services/synoptic/techui-support"] + path = example/t01-services/synoptic/techui-support + url = https://github.com/DiamondLightSource/techui-support.git diff --git a/example/t01-services/synoptic/techui-support b/example/t01-services/synoptic/techui-support deleted file mode 120000 index af7d84be..00000000 --- a/example/t01-services/synoptic/techui-support +++ /dev/null @@ -1 +0,0 @@ -../../../example-synoptic/b23-services/synoptic/techui-support \ No newline at end of file diff --git a/example/t01-services/synoptic/techui-support b/example/t01-services/synoptic/techui-support new file mode 160000 index 00000000..b908b8ff --- /dev/null +++ b/example/t01-services/synoptic/techui-support @@ -0,0 +1 @@ +Subproject commit b908b8ff3cb8b4feebf6fa10697f938e00219587 diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 7b849c65..5935d655 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -8,12 +8,13 @@ import yaml from epicsdbbuilder.recordbase import Record +from jinja2 import Template from lxml import etree, objectify from lxml.objectify import ObjectifiedElement from softioc.builder import records from techui_builder.generate import Generator -from techui_builder.models import Entity, TechUi +from techui_builder.models import Entity, SupportEntity, TechUi, TechUiSupport from techui_builder.validator import Validator logger_ = logging.getLogger(__name__) @@ -49,9 +50,10 @@ class Builder: default_factory=lambda: defaultdict(list), init=False ) status_pvs: dict[str, Record] = field(default_factory=dict, init=False) + + # These are global params for the class (not accessible by user) _services_dir: Path = field(init=False, repr=False) - _gui_map: dict = field(init=False, repr=False) - _write_directory: Path = field(default=Path("opis"), init=False, repr=False) + _write_directory: Path = field(init=False, repr=False) def __post_init__(self): # Populate beamline and components @@ -61,12 +63,30 @@ def __post_init__(self): def setup(self): """Run intial setup, e.g. extracting entries from service ioc.yaml.""" + # This needs to be before _read_map() + self.support_path = self._write_directory.joinpath("techui-support") + + self._read_map() + self._extract_services() - synoptic_dir = self._write_directory self.clean_files() - self.generator = Generator(synoptic_dir, self.conf.beamline.url) + self.generator = Generator( + self._write_directory, + self.conf.beamline.url, + self.support_path, + self.techui_support, + ) + + def _read_map(self): + """Read the techui-support.yaml file from techui-support.""" + support_yaml = self.support_path.joinpath("techui-support.yaml").absolute() + logger_.debug(f"techui-support.yaml location: {support_yaml}") + + self.techui_support = TechUiSupport.model_validate( + yaml.safe_load(support_yaml.read_text(encoding="utf-8")) + ) def clean_files(self): exclude = {"index.bob"} @@ -177,17 +197,28 @@ def _extract_entities(self, service_name: str, ioc_yaml: Path): with open(ioc_yaml) as ioc: ioc_conf: dict[str, list[dict[str, str]]] = yaml.safe_load(ioc) for entity in ioc_conf["entities"]: - if "P" in entity.keys(): + if entity["type"] in self.techui_support.support_modules: + support_mapping: SupportEntity = ( + self.techui_support.support_modules[entity["type"]] + ) + support_macros = support_mapping.macros + + macros = {k: v for k, v in entity.items() if k in support_macros} + + prefix_template = Template(support_mapping.prefix) + prefix: str = prefix_template.render(macros) + # Create Entity and append to entity list new_entity = Entity( service_name=service_name, type=entity["type"], desc=entity.get("desc", None), - P=entity["P"], - M=None if (val := entity.get("M")) is None else val, - R=None if (val := entity.get("R")) is None else val, + prefix=prefix, + macros=macros, ) - self.entities[new_entity.P].append(new_entity) + + pv_root = prefix.split(":", maxsplit=1)[0] + self.entities[pv_root].append(new_entity) def _generate_screen(self, screen_name: str): self.generator.build_screen(screen_name) diff --git a/src/techui_builder/generate.py b/src/techui_builder/generate.py index 80c986d0..c51246d3 100644 --- a/src/techui_builder/generate.py +++ b/src/techui_builder/generate.py @@ -2,17 +2,16 @@ import os import re from collections import defaultdict -from collections.abc import Mapping, Sequence +from collections.abc import Mapping from dataclasses import dataclass, field from pathlib import Path -import yaml from lxml import objectify from phoebusgen import screen as pscreen from phoebusgen import widget as pwidget from phoebusgen.widget.widgets import ActionButton, EmbeddedDisplay, Group -from techui_builder.models import Component, Entity +from techui_builder.models import Component, Entity, TechUiSupport logger_ = logging.getLogger(__name__) @@ -23,12 +22,10 @@ class Generator: beamline_url: str = field(repr=False) # These are global params for the class (not accessible by user) - support_path: Path = field(init=False, repr=False) - techui_support: dict = field(init=False, repr=False) + support_path: Path = field(repr=False) + techui_support: TechUiSupport = field(repr=False) default_size: int = field(default=100, init=False, repr=False) - P: str = field(default="P", init=False, repr=False) - M: str = field(default="M", init=False, repr=False) - R: str = field(default="R", init=False, repr=False) + prefix: str = field(default="P", init=False, repr=False) widgets: list[ActionButton | EmbeddedDisplay] = field( default_factory=list[ActionButton | EmbeddedDisplay], init=False, repr=False ) @@ -42,20 +39,6 @@ class Generator: group_padding: int = field(default=50, init=False, repr=False) label_flag: bool = field(default=False, init=False, repr=False) - def __post_init__(self): - # This needs to be before _read_map() - self.support_path = self.synoptic_dir.joinpath("techui-support") - - self._read_map() - - def _read_map(self): - """Read the techui-support.yaml file from techui-support.""" - support_yaml = self.support_path.joinpath("techui-support.yaml").absolute() - logger_.debug(f"techui-support.yaml location: {support_yaml}") - - with open(support_yaml) as map: - self.techui_support = yaml.safe_load(map) - def _get_screen_dimensions(self, file: str) -> tuple[int, int]: """ Parses the bob files for information on the height @@ -160,88 +143,100 @@ def _get_group_dimensions(self, widget_list: list[EmbeddedDisplay | ActionButton max(width_list) + self.group_padding, ) - def _initialise_name_suffix(self, component: Entity) -> tuple[str, str, str | None]: - if component.M is not None: - name: str = component.M - suffix: str = component.M - suffix_label: str | None = self.M - elif component.R is not None: - name = component.R - suffix = component.R - suffix_label = self.R - else: - name = component.P - suffix = "" - suffix_label = "" + def _update_macros(self, component: Entity) -> tuple[str, dict[str, str]]: + # try statement below is check if the suffix is part of the component prefix. + # If not missing, use as name of widget. If missing, use type as name. + + new_macros = {} + + try: + # re.split() returns the remainder as the final element, + # so this needs to be ignored + prefix, suffix = re.split(r"(:[A-Z0-9:]+)", component.prefix, maxsplit=1)[ + :2 + ] + component_name = suffix.removeprefix(":").removesuffix(":") + suffix_key = next(k for k, v in component.macros.items() if v == suffix) + except (IndexError, ValueError): + prefix = component.prefix + component_name = component.type + suffix_key = suffix = "" - name = name.removeprefix(":").removesuffix(":") # Try to get name from child labels if they exist, # if not, just use the name as it is. if component.child_labels is not None: - if name in component.child_labels.keys(): - name = component.child_labels[name] + if suffix in component.child_labels.keys(): + component_name = component.child_labels[suffix] self.label_flag = True - return (name, suffix, suffix_label) + prefix_key = next(k for k, v in component.macros.items() if v == prefix) - def _is_list_of_dicts(self, scrn_mapping: Mapping) -> bool: - return isinstance(scrn_mapping, Sequence) and all( - isinstance(scrn, Mapping) for scrn in scrn_mapping - ) + new_macros[prefix_key] = prefix + if suffix_key != "": + new_macros[suffix_key] = suffix + new_macros["label"] = component_name + + return component_name, new_macros def _allocate_widget( - self, scrn_mapping: Mapping, component: Entity + self, screen_mapping: Mapping, component: Entity ) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]: - name, suffix, suffix_label = self._initialise_name_suffix(component) + component_name, updated_macros = self._update_macros(component) # Get relative path to screen - file = scrn_mapping["file"] + file = screen_mapping["file"] if file.startswith("$(IOC)"): - scrn_path = data_scrn_path = file.replace( + screen_path = support_screen_path = file.replace( "$(IOC)", f"{self.beamline_url}/{component.service_name}" ) # Only works with related displays as # embedded displays need to access the file to get dimensions - assert scrn_mapping["type"] == "related", ( + assert screen_mapping["type"] == "related", ( "Only related displays can have remote screens" ) else: - scrn_path = self.support_path.joinpath(f"bob/{file}") - logger_.debug(f"Screen path: {scrn_path}") + screen_path = self.support_path.joinpath(f"bob/{file}") + logger_.debug(f"Screen path: {screen_path}") - # Path of screen relative to data/ so it knows where to open the file from - data_scrn_path = scrn_path.relative_to(self.synoptic_dir, walk_up=True) + # Path of screen relative to synoptic/ + support_screen_path = screen_path.relative_to( + self.synoptic_dir, walk_up=True + ) # For Gui Components with multiple components embedded, we add a suffix field # to the components, and adjust the name and suffix accordingly try: - if scrn_mapping["suffix"] is not None: - suffix: str = scrn_mapping["suffix"] - match: re.Match[str] | None = re.match( - r"^\$\(([A-Z])\)\$\(([A-Z])\)$", scrn_mapping["prefix"] - ) - if match: - suffix_label: str | None = match.group(2) - if self.label_flag is False: - name = suffix + if screen_mapping["suffixes"] is not None: + suffix_dict: dict[str, str] = screen_mapping["suffixes"] + for suffix_key, suffix in suffix_dict.items(): + updated_macros[suffix_key] = suffix + + # If no child label was specified... + if self.label_flag is False: + # TODO: think of a better fallback component name for this + component_name = ( + list(suffix_dict.values())[0] + .removeprefix(":") + .removesuffix(":") + ) + updated_macros["label"] = component_name except KeyError: pass - if scrn_mapping["type"] == "embedded": - height, width = self._get_screen_dimensions(str(scrn_path)) + if screen_mapping["type"] == "embedded": + height, width = self._get_screen_dimensions(str(screen_path)) new_widget = pwidget.EmbeddedDisplay( - name.removeprefix(":").removesuffix(":"), - str(data_scrn_path), + component_name, + str(support_screen_path), 0, 0, # Change depending on the order width, height, ) # Add macros to the widgets - new_widget.macro(self.P, component.P) - if suffix_label != "": - new_widget.macro(f"{suffix_label}", suffix) - new_widget.macro("label", name.removeprefix(":").removesuffix(":")) + for macro, macro_val in updated_macros.items(): + new_widget.macro(macro, macro_val) + # TODO: Change this to pvi_button if True: new_widget.macro("IOC", f"{self.beamline_url}/{component.service_name}") @@ -251,8 +246,8 @@ def _allocate_widget( height, width = (40, 100) new_widget = pwidget.ActionButton( - name.removeprefix(":").removesuffix(":"), - name.removeprefix(":").removesuffix(":"), + component_name, + component_name, "", 0, 0, @@ -261,40 +256,23 @@ def _allocate_widget( ) # Add action to action button: to open related display - if suffix_label != "": - new_widget.action_open_display( - file=str(data_scrn_path), - target="tab", - macros={ - "P": component.P, - f"{suffix_label}": suffix, - }, - ) - else: - new_widget.action_open_display( - file=str(data_scrn_path), - target="tab", - macros={ - "P": component.P, - }, - ) + + new_widget.action_open_display( + file=str(support_screen_path), target="tab", macros=updated_macros + ) # For some reason the version of action buttons is 3.0.0? new_widget.version("2.0.0") self.label_flag = False return new_widget - def _create_widget( + def _create_widgets( self, name: str, component: Entity - ) -> EmbeddedDisplay | ActionButton | None | list[EmbeddedDisplay | ActionButton]: - # if statement below is check if the suffix is - # missing from the component description. If - # not missing, use as name of widget, if missing, - # use type as name. + ) -> list[EmbeddedDisplay | ActionButton] | None: new_widget = [] try: - scrn_mapping = self.techui_support[component.type] + screen_mapping = self.techui_support.support_modules[component.type].screens except KeyError: logger_.warning( f"No available widget for {component.type} in screen \ @@ -302,11 +280,8 @@ def _create_widget( ) return None - if self._is_list_of_dicts(scrn_mapping): - for value in scrn_mapping: - new_widget.append(self._allocate_widget(value, component)) - else: - new_widget = self._allocate_widget(scrn_mapping, component) + for screen_dict in screen_mapping: + new_widget.append(self._allocate_widget(screen_dict, component)) return new_widget @@ -374,20 +349,17 @@ def layout_widgets(self, widgets: list[EmbeddedDisplay | ActionButton]): return sorted_widgets - def build_widgets(self, screen_name: str, screen_components: list[Entity]): + def build_widgets(self, screen_name: str, screen_entities: list[Entity]): # Empty widget buffer self.widgets = [] # order is an enumeration of the components, used to list them, # and serves as functionality in the math for formatting. - for component in screen_components: - new_widget = self._create_widget(name=screen_name, component=component) - if new_widget is None: - continue - if isinstance(new_widget, list): - self.widgets.extend(new_widget) + for entity in screen_entities: + new_widgets = self._create_widgets(name=screen_name, component=entity) + if new_widgets is None: continue - self.widgets.append(new_widget) + self.widgets.extend(new_widgets) def build_groups(self, screen_name: str, builder_components: dict[str, Component]): """ diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index 61e64409..20130b4e 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -1,6 +1,6 @@ import logging import re -from typing import Annotated, Literal +from typing import Annotated, Any, Literal from pydantic import ( BaseModel, @@ -272,7 +272,7 @@ class Entity(BaseModel): "ADAravis.aravisCamera" ), ] - P: Annotated[str, Field(description="PV Prefix for module entity")] + prefix: Annotated[str, Field(description="PV Prefix for module entity")] desc: Annotated[ str | None, Field(description="Optional description of module entity") ] = None @@ -280,7 +280,30 @@ class Entity(BaseModel): dict[str, str] | None, Field(description="Optional child labels for module entity"), ] = None - M: Annotated[str | None, Field(description="Optional PV suffix for a motor")] - R: Annotated[ - str | None, Field(description="Optional PV suffix for an ADAravis plugin") + macros: Annotated[ + dict[str, Any], + Field(description="Macros for the matching screen (can be empty)"), + ] + + +class SupportEntity(BaseModel): + """ + Table of variables from corresponding support module in techui-support.yaml file + """ + + prefix: Annotated[str, Field(description="Prefix for techui-support screen")] + macros: Annotated[ + list[str], + Field(description="Macros for the matching screen (can be empty)"), + ] + screens: Annotated[ + list[dict[str, str | dict[str, str]]], + Field(description="Dictionary of available screens for the support module"), + ] + + +class TechUiSupport(BaseModel): + support_modules: Annotated[ + dict[str, SupportEntity], + Field(description="The dictionary of techui-support.yaml entities"), ] diff --git a/tests/conftest.py b/tests/conftest.py index b0125ef2..37b2eaae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,15 @@ from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from lxml.etree import Element, SubElement, tostring from lxml.objectify import fromstring +from phoebusgen import widget as pwidget from techui_builder.autofill import Autofiller from techui_builder.builder import Builder, JsonMap from techui_builder.generate import Generator -from techui_builder.models import Component +from techui_builder.models import Component, SupportEntity from techui_builder.validator import Validator @@ -24,10 +25,77 @@ def builder(): @pytest.fixture -def builder_with_setup(builder: Builder): +def techui_support(): + ts = MagicMock() + ts.support_modules = { + "pmac.GeoBrick": SupportEntity(prefix="{{ P }}", macros=["P"], screens=[{}]), + "pmac.autohome": SupportEntity(prefix="{{ P }}", macros=["P"], screens=[{}]), + "pmac.dls_pmac_asyn_motor": SupportEntity( + prefix="{{ P }}{{ M }}", macros=["P", "M"], screens=[{}] + ), + "ADAravis.aravisCamera": SupportEntity( + prefix="{{ P }}{{ R }}", + macros=["P", "R"], + screens=[ + {"file": "ADAravis/ADAravis_summary.bob", "type": "embedded"}, + {"file": "ADAravis/ADAravis_detail.bob", "type": "related"}, + ], + ), + "ADUVC.UVC": SupportEntity( + prefix="{{ P }}{{ R }}", + macros=["P", "R"], + screens=[ + {"file": "ADUVC/ADUVC_summary.bob", "type": "embedded"}, + {"file": "$(IOC)/ADUVC.pvi.bob", "type": "related"}, + ], + ), + "detectorPlugins.detectorPlugins": SupportEntity( + prefix="{{ P }}{{ R }}", + macros=["P", "R"], + screens=[ + { + "file": "ADAravis/NDPluginStats.pvi.bob", + "suffixes": { + "R": ":STAT:", + }, + "type": "related", + }, + { + "file": "ADAravis/NDPluginPva.pvi.bob", + "suffixes": { + "R": ":PVA:", + }, + "type": "related", + }, + { + "file": "ADAravis/NDPluginROIStat.pvi.bob", + "suffixes": { + "R": ":ROISTAT:", + }, + "type": "related", + }, + { + "file": "ADAravis/NDFileHDF5.pvi.bob", + "suffixes": { + "R": ":HDF5:", + }, + "type": "related", + }, + ], + ), + } + + return ts + + +@pytest.fixture +def builder_with_setup(builder: Builder, techui_support): with patch("techui_builder.builder.Generator") as mock_generator: mock_generator.return_value = MagicMock() + builder._read_map = Mock() + builder.techui_support = techui_support + builder.setup() return builder @@ -121,10 +189,11 @@ def example_display_names_json(): @pytest.fixture -def generator(): +def generator(techui_support): synoptic_dir = Path(__file__).parent.joinpath(Path("t01-services/synoptic")) + techui_support_path = synoptic_dir.joinpath("techui-support") - g = Generator(synoptic_dir, "test_url") + g = Generator(synoptic_dir, "test_url", techui_support_path, techui_support) return g @@ -147,7 +216,7 @@ def validator(): @pytest.fixture -def example_embedded_widget(): +def example_xml_embedded_widget(): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -173,7 +242,7 @@ def example_embedded_widget(): @pytest.fixture -def example_related_widget(): +def example_xml_related_widget(): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -207,7 +276,7 @@ def example_related_widget(): @pytest.fixture -def example_symbol_widget(): +def example_xml_symbol_widget(): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... widget_element = Element("widget") @@ -227,7 +296,7 @@ def example_symbol_widget(): @pytest.fixture -def example_navtabs_widget(): +def example_xml_navtabs_widget(): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -264,3 +333,32 @@ def example_navtabs_widget(): widget_element = fromstring(tostring(widget_element)) return widget_element + + +@pytest.fixture +def example_pgen_embedded_widget(): + embedded_widget = pwidget.EmbeddedDisplay( + "CAM", "techui-support/bob/ADAravis/ADAravis_summary.bob", 0, 0, 860, 450 + ) + embedded_widget.macro("P", "BL01T-DI-IOC-01") + embedded_widget.macro("R", ":CAM:") + embedded_widget.macro("label", "CAM") + embedded_widget.macro("IOC", "test_url/bl01t-di-ioc-01") + + return embedded_widget + + +@pytest.fixture +def example_pgen_related_widget(): + related_widget = pwidget.ActionButton( + "BRICK", "BRICK", "", x=0, y=0, width=100, height=40 + ) + related_widget.action_open_display( + file="techui-support/bob/pmac/pmacController.bob", + target="tab", + macros={"P": "BL01T-MO-IOC-01"}, + ) + # For some reason the version of action buttons is 3.0.0? + related_widget.version("2.0.0") + + return related_widget diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index 81af83fd..a6ba49e9 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -74,7 +74,7 @@ def test_autofiller_write_bob(mock_tree, mock_deannotate, autofiller): def test_autofiller_replace_content( mock_get, autofiller, - example_related_widget, + example_xml_related_widget, prefix, description, filename, @@ -82,7 +82,7 @@ def test_autofiller_replace_content( expected_desc, expected_file, ): - mock_get.return_value = example_related_widget.actions.action + mock_get.return_value = example_xml_related_widget.actions.action # Cannot use a Mock object as need P to be computed fake_component = Component( @@ -93,17 +93,17 @@ def test_autofiller_replace_content( ) autofiller.replace_content( - example_related_widget, + example_xml_related_widget, "test_component", fake_component, ) - assert example_related_widget.pv_name == f"{prefix}:STA" - assert example_related_widget.actions.action.description.text == expected_desc - assert example_related_widget.actions.action.file.text == expected_file + assert example_xml_related_widget.pv_name == f"{prefix}:STA" + assert example_xml_related_widget.actions.action.description.text == expected_desc + assert example_xml_related_widget.actions.action.file.text == expected_file if macros is not None: for k, v in macros.items(): - assert example_related_widget.actions.action.macros[k] == macros[k] == v + assert example_xml_related_widget.actions.action.macros[k] == macros[k] == v @patch("techui_builder.autofill._get_action_group") diff --git a/tests/test_builder.py b/tests/test_builder.py index aee743ed..82a7c47c 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -153,39 +153,44 @@ def test_missing_service(builder, caplog): @pytest.mark.parametrize( - "index, type, desc, P, M, R", + "index, type, desc, pv, macros", [ - (0, "pmac.GeoBrick", None, "BL01T-MO-BRICK-01", None, None), - (0, "pmac.autohome", None, "BL01T-MO-MOTOR-01", None, None), + (0, "pmac.GeoBrick", None, "BL01T-MO-BRICK-01", {"P": "BL01T-MO-BRICK-01"}), ( - 1, - "pmac.dls_pmac_asyn_motor", + 0, + "pmac.autohome", None, "BL01T-MO-MOTOR-01", - ":X", + {"P": "BL01T-MO-MOTOR-01"}, + ), + ( + 1, + "pmac.dls_pmac_asyn_motor", None, + "BL01T-MO-MOTOR-01:X", + {"P": "BL01T-MO-MOTOR-01", "M": ":X"}, ), ( 2, "pmac.dls_pmac_asyn_motor", None, - "BL01T-MO-MOTOR-01", - ":A", - None, + "BL01T-MO-MOTOR-01:A", + {"P": "BL01T-MO-MOTOR-01", "M": ":A"}, ), ], ) -def test_gb_extract_entities(builder, index, type, desc, P, M, R): # noqa: N803 - builder._extract_entities( +def test_gb_extract_entities(builder_with_setup, index, type, desc, pv, macros): # noqa: N803 + prefix = pv.split(":", maxsplit=1)[0] + + builder_with_setup._extract_entities( "bl01t-mo-ioc-01", - builder._services_dir.joinpath("bl01t-mo-ioc-01/config/ioc.yaml"), + builder_with_setup._services_dir.joinpath("bl01t-mo-ioc-01/config/ioc.yaml"), ) - entity = builder.entities[P][index] + entity = builder_with_setup.entities[prefix][index] assert entity.type == type assert entity.desc == desc - assert entity.P == P - assert entity.M == M - assert entity.R == R + assert entity.prefix == pv + assert entity.macros == macros def test_builder_generate_screen(builder_with_setup): @@ -581,8 +586,8 @@ def test_get_component_label_with_current_component_name_invalid( assert display_name == "new_name" -def test_get_nav_tabs(example_navtabs_widget): - tabs_widget = _get_nav_tabs(example_navtabs_widget) +def test_get_nav_tabs(example_xml_navtabs_widget): + tabs_widget = _get_nav_tabs(example_xml_navtabs_widget) assert isinstance(tabs_widget, list) diff --git a/tests/test_files/widget_custom_suffix.xml b/tests/test_files/widget_custom_suffix.xml new file mode 100644 index 00000000..a16368a4 --- /dev/null +++ b/tests/test_files/widget_custom_suffix.xml @@ -0,0 +1,22 @@ + + + STAT + 0 + 0 + 100 + 40 + + STAT + + + Open Display + +

BL01T-DI-IOC-01

+ :STAT: + +
+ techui-support/bob/ADAravis/NDPluginStats.pvi.bob + tab +
+
+
diff --git a/tests/test_files/widget_related.xml b/tests/test_files/widget_related.xml index 8903a997..cca61694 100644 --- a/tests/test_files/widget_related.xml +++ b/tests/test_files/widget_related.xml @@ -1,18 +1,17 @@ - M + BRICK 0 0 100 40 - M + BRICK Open Display

BL01T-MO-IOC-01

- :M
techui-support/bob/pmac/pmacController.bob tab diff --git a/tests/test_files/widget_url_screen.xml b/tests/test_files/widget_url_screen.xml index c20d93d5..9def9f7d 100644 --- a/tests/test_files/widget_url_screen.xml +++ b/tests/test_files/widget_url_screen.xml @@ -13,8 +13,9 @@

BL01T-DI-IOC-01

:CAM: +
- test_url/bl01t-di-ioc-01/ADAravis_summary.bob + test_url/bl01t-di-ioc-01/ADUVC.pvi.bob tab
diff --git a/tests/test_generate.py b/tests/test_generate.py index 7a4bc8f2..ddd3309c 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -106,19 +106,18 @@ def test_generator_get_group_dimensions(generator): assert width == 300 -def test_generator_create_widget_keyerror(generator, caplog): +def test_generator_create_widgets_keyerror(generator, caplog): generator._get_screen_dimensions = Mock(return_value=(800, 1280)) screen_name = "test" component = Entity( service_name="bl01t-di-ioc-01", type="key.notavailable", - P="BL01T-DI-IOC-01", + prefix="BL01T-DI-IOC-01:CAM:", desc=None, - M=None, - R=":CAM:", + macros={"P": "BL01T-DI-IOC-01", "R": ":CAM:"}, ) - result = generator._create_widget(name=screen_name, component=component) + result = generator._create_widgets(name=screen_name, component=component) assert result is None assert ( @@ -127,43 +126,19 @@ def test_generator_create_widget_keyerror(generator, caplog): ) -def test_generator_create_widget_is_list_of_dicts(generator): - generator._get_screen_dimensions = Mock(return_value=(800, 1280)) - generator._is_list_of_dicts = Mock(return_value=True) - generator._allocate_widget = Mock( - return_value=pwidget.EmbeddedDisplay( - name="X", file="", x=0, y=0, width=205, height=120 - ) - ) - screen_name = "test" - component = Entity( - service_name="bl01t-di-ioc-01", - type="ADAravis.aravisCamera", - P="BL01T-DI-IOC-01", - desc=None, - M=None, - R=":CAM:", - ) - widget = generator._create_widget(name=screen_name, component=component) - for value in widget: - assert str(value) == str( - pwidget.EmbeddedDisplay(name="X", file="", x=0, y=0, width=205, height=120) - ) - +def test_generator_create_widgets_embedded(generator, example_pgen_embedded_widget): + generator._allocate_widget = Mock(return_value=example_pgen_embedded_widget) -def test_generator_create_widget_embedded(generator): - generator._get_screen_dimensions = Mock(return_value=(450, 860)) screen_name = "test" component = Entity( service_name="bl01t-di-ioc-01", type="ADAravis.aravisCamera", - P="BL01T-DI-IOC-01", + prefix="BL01T-DI-IOC-01:CAM:", desc=None, - M=None, - R=":CAM:", + macros={"P": "BL01T-DI-IOC-01", "R": ":CAM:"}, ) - widget = generator._create_widget( + widget = generator._create_widgets( name=screen_name, component=component, ) @@ -176,85 +151,79 @@ def test_generator_create_widget_embedded(generator): assert str(widget[0]) == xml_content -def test_generator_initialise_name_suffix_m(generator): - component = Entity( - service_name="bl01t-mo-ioc-01", type="test", P="TEST", desc=None, M="T1", R=None - ) - - name, suffix, suffix_label = generator._initialise_name_suffix(component) - - assert name == "T1" - assert suffix == "T1" - assert suffix_label == "M" - +def test_generator_update_macros(generator): + suffix_key = "M" + suffix = ":T1" -def test_generator_initialise_name_suffix_r(generator): component = Entity( - service_name="bl01t-di-ioc-01", type="test", P="TEST", desc=None, M=None, R="T1" + service_name="bl01t-mo-ioc-01", + type="test", + prefix="TEST:T1", + desc=None, + macros={"P": "TEST", suffix_key: suffix}, ) - name, suffix, suffix_label = generator._initialise_name_suffix(component) + component_name, updated_macros = generator._update_macros(component) - assert name == "T1" - assert suffix == "T1" - assert suffix_label == "R" + assert component_name == "T1" + assert updated_macros[suffix_key] == suffix + assert updated_macros["label"] == "T1" -def test_generator_initialise_name_suffix_none(generator): +def test_generator_update_macros_no_suffix(generator): component = Entity( - service_name="bl01t-ea-ioc-01", type="test", P="TEST", desc=None, M=None, R=None + service_name="bl01t-ea-ioc-01", + type="test", + prefix="TEST", + desc=None, + macros={"pv": "TEST"}, ) - name, suffix, suffix_label = generator._initialise_name_suffix(component) + component_name, updated_macros = generator._update_macros(component) - assert name == component.P - assert suffix == "" - assert suffix_label == "" + assert component_name == "test" + assert len(updated_macros) == 1 + assert updated_macros["pv"] == "TEST" + assert "label" not in updated_macros.keys() -def test_generator_initialise_name_suffix_with_child_labels(generator): +def test_generator_update_macros_suffix_with_child_labels(generator): + suffix_key = "R" + suffix = ":T1" + child_label = "Test 1" + component = Entity( type="test", - P="TEST", + prefix="TEST:T1", desc=None, service_name="bl01t-mo-test-01", - M=None, - R="T1", - child_labels={"T1": "Test 1"}, + macros={"P": "TEST", suffix_key: suffix}, + child_labels={suffix: child_label}, ) - name, suffix, suffix_label = generator._initialise_name_suffix(component) - - assert name == "Test 1" - assert suffix == "T1" - assert suffix_label == "R" - + component_name, updated_macros = generator._update_macros(component) -def test_generator_is_list_of_dicts(generator): - list_of_dicts = [{"a": 1}, {"b": 2}] - assert generator._is_list_of_dicts(list_of_dicts) is True - - -def test_generator_is_list_of_dicts_not(generator): - not_list_of_dicts = {"a": 1} - assert generator._is_list_of_dicts(not_list_of_dicts) is False + assert component_name == child_label + assert updated_macros["label"] == child_label def test_generator_allocate_widget(generator): - generator._initilise_name_suffix = Mock(return_value=("CAM:", "CAM:", "R")) + generator._update_macros = Mock( + return_value=("CAM", {"P": "BL01T-DI-IOC-01", "R": ":CAM:", "label": "CAM"}) + ) + generator._get_screen_dimensions = Mock(return_value=(450, 860)) + + scrn_mappings = generator.techui_support.support_modules[ + "ADAravis.aravisCamera" + ].screens + scrn_mapping = next((x for x in scrn_mappings if x["type"] == "embedded"), None) - scrn_mapping = { - "file": "ADAravis/ADAravis_summary.bob", - "prefix": "$(P)$(R)", - "type": "embedded", - } component = Entity( service_name="bl01t-di-ioc-01", type="ADAravis.aravisCamera", - P="BL01T-DI-IOC-01", + prefix="BL01T-DI-IOC-01:CAM:", desc=None, - M=None, - R=":CAM:", + macros={"P": "BL01T-DI-IOC-01", "R": ":CAM:"}, ) widget = generator._allocate_widget(scrn_mapping, component) control_widget = Path("tests/test_files/widget.xml") @@ -266,20 +235,19 @@ def test_generator_allocate_widget(generator): def test_generator_allocate_widget_with_remote_screens(generator): - generator._initilise_name_suffix = Mock(return_value=("CAM:", "CAM:", "R")) + generator._update_macros = Mock( + return_value=("CAM", {"P": "BL01T-DI-IOC-01", "R": ":CAM:", "label": "CAM"}) + ) + + scrn_mappings = generator.techui_support.support_modules["ADUVC.UVC"].screens + scrn_mapping = next((x for x in scrn_mappings if x["type"] == "related"), None) - scrn_mapping = { - "file": "$(IOC)/ADAravis_summary.bob", - "prefix": "$(P)$(R)", - "type": "related", - } component = Entity( service_name="bl01t-di-ioc-01", - type="ADAravis.aravisCamera", - P="BL01T-DI-IOC-01", + type="ADUVC.UVC", + prefix="BL01T-DI-IOC-01:CAM:", desc=None, - M=None, - R=":CAM:", + macros={"P": "BL01T-DI-IOC-01", "R": ":CAM:"}, ) widget = generator._allocate_widget(scrn_mapping, component) control_widget = Path("tests/test_files/widget_url_screen.xml") @@ -290,25 +258,24 @@ def test_generator_allocate_widget_with_remote_screens(generator): assert str(widget) == xml_content -def test_generator_allocate_widget_with_suffix(generator): - generator._initialise_name_suffix = Mock(return_value=(":CAM:", ":CAM:", "R")) +def test_generator_allocate_widget_with_custom_suffix(generator): + generator._update_macros = Mock(return_value=("CAM", {"P": "BL01T-DI-IOC-01"})) + generator._get_screen_dimensions = Mock(return_value=(40, 100)) + + scrn_mappings = generator.techui_support.support_modules[ + "detectorPlugins.detectorPlugins" + ].screens + scrn_mapping = next((x for x in scrn_mappings if x["type"] == "related"), None) - scrn_mapping = { - "file": "ADAravis/ADAravis_summary.bob", - "prefix": "$(P)$(R)", - "suffix": ":CAM:", - "type": "embedded", - } component = Entity( service_name="bl01t-di-ioc-01", type="detectorPlugins.detectorPlugins", - P="BL01T-DI-IOC-01", + prefix="BL01T-DI-IOC-01", desc=None, - M=None, - R=None, + macros={"P": "BL01T-DI-IOC-01"}, ) widget = generator._allocate_widget(scrn_mapping, component) - control_widget = Path("tests/test_files/widget.xml") + control_widget = Path("tests/test_files/widget_custom_suffix.xml") with open(control_widget) as f: xml_content = f.read() @@ -316,51 +283,53 @@ def test_generator_allocate_widget_with_suffix(generator): assert str(widget) == xml_content -def test_generator_create_widget_related(generator): +def test_generator_create_widgets_related(generator, example_pgen_related_widget): + generator._allocate_widget = Mock(return_value=example_pgen_related_widget) generator._get_screen_dimensions = Mock(return_value=(800, 1280)) - screen_name = "test" + component = Entity( service_name="bl01t-mo-ioc-01", type="pmac.GeoBrick", - P="BL01T-MO-IOC-01", + prefix="BL01T-MO-IOC-01", desc=None, - M=":M", - R=None, + macros={"P": "BL01T-MO-IOC-01"}, ) - widget = generator._create_widget( - name=screen_name, + widgets = generator._create_widgets( + name="BRICK", component=component, ) control_widget = Path("tests/test_files/widget_related.xml") with open(control_widget) as f: xml_content = f.read() - assert str(widget) == xml_content + assert str(widgets[0]) == xml_content -def test_generator_create_widget_related_no_suffix(generator): - generator._get_screen_dimensions = Mock(return_value=(800, 1280)) - screen_name = "test" - component = Entity( - service_name="bl01t-mo-ioc-01", - type="pmac.GeoBrick", - P="BL01T-MO-IOC-01", - desc=None, - M=None, - R=None, - ) +# def test_generator_create_widgets_related_no_suffix( +# generator, example_pgen_related_widget +# ): +# generator._allocate_widget = Mock(return_value=example_pgen_related_widget) +# generator._get_screen_dimensions = Mock(return_value=(800, 1280)) - widget = generator._create_widget( - name=screen_name, - component=component, - ) +# component = Entity( +# service_name="bl01t-mo-ioc-01", +# type="pmac.GeoBrick", +# prefix="BL01T-MO-IOC-01", +# desc=None, +# macros={"P": "BL01T-MO-IOC-01"}, +# ) - control_widget = Path("tests/test_files/widget_related_no_suffix.xml") +# widgets = generator._create_widgets( +# name="BRICK", +# component=component, +# ) - with open(control_widget) as f: - xml_content = f.read() - assert str(widget) == xml_content +# control_widget = Path("tests/test_files/widget_related_no_suffix.xml") + +# with open(control_widget) as f: +# xml_content = f.read() +# assert str(widgets[0]) == xml_content @pytest.mark.parametrize( @@ -403,7 +372,7 @@ def test_generator_layout_widgets(generator, index, x, y): # TODO: Split up test def test_generator_build_screen(generator, components): - generator._create_widget = Mock(return_value=Mock()) + generator._create_widgets = Mock(return_value=[Mock()]) generator.layout_widgets = Mock( return_value=[ pwidget.EmbeddedDisplay(name="X", file="", x=0, y=0, width=205, height=120), @@ -425,7 +394,7 @@ def test_generator_build_screen(generator, components): def test_build_groups_with_label(generator, components): screen_name = "motor" generator.widgets = [Mock(), Mock(), Mock()] - generator._create_widget = Mock(return_value=Mock()) + generator._create_widgets = Mock(return_value=Mock()) generator.layout_widgets = Mock( return_value=[ pwidget.EmbeddedDisplay(name="X", file="", x=0, y=0, width=205, height=120), @@ -444,7 +413,7 @@ def test_build_groups_with_label(generator, components): def test_build_groups(generator, components): screen_name = "test" generator.widgets = [Mock(), Mock(), Mock()] - generator._create_widget = Mock(return_value=Mock()) + generator._create_widgets = Mock(return_value=Mock()) generator.layout_widgets = Mock( return_value=[ pwidget.EmbeddedDisplay(name="X", file="", x=0, y=0, width=205, height=120), diff --git a/tests/test_utils.py b/tests/test_utils.py index 1c97092f..13a675e1 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -18,9 +18,9 @@ def test_read_bob(mock_get_widgets): mock_get_widgets.assert_called_once() -def test_get_widgets(example_symbol_widget): +def test_get_widgets(example_xml_symbol_widget): test_root = Element("root") - test_root.append(example_symbol_widget) + test_root.append(example_xml_symbol_widget) widgets = get_widgets(test_root) diff --git a/tests/test_validator.py b/tests/test_validator.py index fc1204e6..86278379 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -31,11 +31,11 @@ def test_validator_read_bob(mock_read_bob, validator): # TODO: Clean up this test... (make fixture for mock xml?) -def test_validator_validate_bob(validator, example_embedded_widget): +def test_validator_validate_bob(validator, example_xml_embedded_widget): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... mock_root_element = Element("root") - mock_root_element.append(example_embedded_widget) + mock_root_element.append(example_xml_embedded_widget) # ... which requires this horror mock_element = fromstring(tostring(mock_root_element)) # mock_element = ObjectifiedElement(mock_widget_element)