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)