diff --git a/example/t01-services/synoptic/index.bob b/example/t01-services/synoptic/index.bob index e6cd7aca..ee555aa6 100644 --- a/example/t01-services/synoptic/index.bob +++ b/example/t01-services/synoptic/index.bob @@ -1,7 +1,13 @@ - - + + Display + + bl01t + t01 + Test Beamline + t01-opis.diamond.ac.uk + dcam1_Group 250 @@ -12,6 +18,7 @@ true dcam1 + BL01T-DI-CAM-01:STA techui-support/symbols/camera.svg @@ -26,11 +33,10 @@ replace + true $(pv_name) $(pv_value) $(actions) - BL01T-DI-CAM-01:STA - true dcam1_L:abel @@ -50,7 +56,8 @@ $(actions) true - diode1 + {name= diode1, label= Diode 1, extras= [BL01T-DI-PHDGN-01, BL01T-DI-PHDGM-01]} + BL01T-DI-PHDGN-01:STA techui-support/symbols/oscilloscope.svg @@ -65,11 +72,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - BL01T-DI-PHDGN-01:STA - true diode1_Label @@ -102,10 +108,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - true temp1_label @@ -141,10 +147,10 @@ $(actions) replace + true $(pv_name) $(pv_value) $(actions) - true motor1_label diff --git a/src/techui_builder/__main__.py b/src/techui_builder/__main__.py index 97a4fb4f..1cab69cd 100644 --- a/src/techui_builder/__main__.py +++ b/src/techui_builder/__main__.py @@ -31,7 +31,7 @@ `-- synoptic\n . |-- techui-support/\n | | `-- ...\n - . |-- techui.yaml\n + . |-- techui.yaml?\n . `-- index.bob\n """, no_args_is_help=True, diff --git a/src/techui_builder/autofill.py b/src/techui_builder/autofill.py index e7108a5f..1634886b 100644 --- a/src/techui_builder/autofill.py +++ b/src/techui_builder/autofill.py @@ -35,7 +35,7 @@ class Autofiller: def read_bobs(self) -> None: for path in self.index_paths: - tree, widget_dict = read_bob(path) + tree, widget_dict, _ = read_bob(path) self.index_trees[path] = (tree, widget_dict) def autofill_bobs(self) -> None: @@ -69,7 +69,7 @@ def _autofill_from_path(self, path: Path): self._autofill_from_path(resolved_path) else: if resolved_path.exists(): - nav_tree, nav_widgets = read_bob(resolved_path) + nav_tree, nav_widgets, _ = read_bob(resolved_path) self.index_trees[resolved_path] = ( nav_tree, nav_widgets, diff --git a/src/techui_builder/bob_parser.py b/src/techui_builder/bob_parser.py new file mode 100644 index 00000000..6c49a01e --- /dev/null +++ b/src/techui_builder/bob_parser.py @@ -0,0 +1,95 @@ +import logging +from dataclasses import dataclass +from pathlib import Path + +from lxml.objectify import ObjectifiedElement + +from techui_builder.models import BobFile, BobWidget, MacroMap +from techui_builder.utils import _INDEX_BOB_NAME_RE, WidgetType, read_bob + +logger_ = logging.getLogger(__name__) + + +@dataclass +class BobParser: + """ + The BobParser class is responsible for parsing .bob files and extracting relevant + information such as widgets and macros. It provides methods to read the .bob file, + parse its contents, and return a structured representation of the data. + """ + + bob_path: Path + + def _parse_widgets(self, container: ObjectifiedElement) -> list[BobWidget]: + """ + Parses the widgets from the given container element + and returns a list of BobWidget instances. + """ + result = [] + + for element in container.iterchildren("widget"): + if element.get("type", default="") == WidgetType.GROUP: + self._parse_widgets(element) + if element.get("type", default="") not in WidgetType: + continue + # Find the name and prefix of the widget + name_element = element.find("name") + name = ( + name_element.text + if (name_element is not None and name_element.text is not None) + else "" + ) + prefix_element = element.find("pv_name") + prefix = prefix_element.text if prefix_element is not None else "" + + macros: MacroMap = {} + matches = _INDEX_BOB_NAME_RE.finditer(name) + macro_pairs = { + match.group("key") or "name": match.group("val") for match in matches + } + + if macro_pairs is None: + print(f"No match for {name!r}") + continue + + for key, value in macro_pairs.items(): + print(f"Match: {key, value}") + + list_types = ("extras", "status") + if key not in list_types: + macros[key] = value + else: + value = value[1:-1] + macros[key] = [v.strip() for v in value.split(",") if v.strip()] + + # Create a BobWidget instance for the widget + widget = BobWidget( + name=name or "", + widget_type=element.get("type", default=""), + macros=macros, + element=element, + children=None, + prefix=prefix or "", + ) + + if widget.widget_type in WidgetType: + result.append(widget) + + return result + + def parse_bob( + self, + ) -> BobFile: + """ + Parses the .bob file specified by the bob_path + attribute and returns a BobFile instance + """ + tree, _, display_macros = read_bob(self.bob_path) + root = tree.getroot() + + return BobFile( + path=self.bob_path, + macros=display_macros, + tree=tree, + widgets=self._parse_widgets(root), + ) diff --git a/src/techui_builder/builder.py b/src/techui_builder/builder.py index 7702594d..4de15f54 100644 --- a/src/techui_builder/builder.py +++ b/src/techui_builder/builder.py @@ -8,7 +8,14 @@ from jinja2 import Template from techui_builder.generate import Generator -from techui_builder.models import Entity, SupportEntity, TechUi, TechUiSupport +from techui_builder.models import ( + BobFile, + Component, + Entity, + SupportEntity, + TechUi, + TechUiSupport, +) from techui_builder.validator import Validator logger_ = logging.getLogger(__name__) @@ -31,17 +38,45 @@ class Builder: """ - techui: Path = field(default=Path("techui.yaml")) + bob_description: BobFile + techui: Path | None = field(init=True, repr=False) entities: dict[str, Entity] = field(default_factory=dict, init=False) _services_dir: Path = field(init=False, repr=False) _write_directory: Path = field(init=False, repr=False) + def _iter_prefixed_widgets(self, widgets): + for widget in widgets: + if widget.prefix: + yield widget + def __post_init__(self): - # Populate beamline and components - self.conf = TechUi.model_validate( - yaml.safe_load(self.techui.read_text(encoding="utf-8")) - ) + components = {} + + for widget in self._iter_prefixed_widgets(self.bob_description.widgets): + values = { + key: value + for key, value in widget.macros.items() + if key in Component.model_fields and key != "prefix" + } + values["prefix"] = widget.prefix.strip() + values.setdefault("label", widget.name) + components[widget.name] = values + + macros = self.bob_description.macros + techui_data = { + "beamline": { + key: macros[key] for key in ("location", "domain", "desc", "url") + }, + "components": components, + } + + if self.techui is not None and self.techui.is_file(): + yaml_data = yaml.safe_load(self.techui.read_text(encoding="utf-8")) or {} + techui_data["beamline"].update(yaml_data.get("beamline", {})) + components.update(yaml_data.get("components", {})) + + self.conf = TechUi.model_validate(techui_data) def setup(self): """ @@ -59,7 +94,7 @@ def setup(self): self.generator = Generator( self._write_directory, - self.conf.beamline.url, + self.bob_description.macros["url"], self.support_path, self.techui_support, ) @@ -106,7 +141,9 @@ def _extract_services(self): """ # Loop over every dir in services, ignoring anything that isn't a service - for service in self._services_dir.glob(f"{self.conf.beamline.location}-*-*-*"): + for service in self._services_dir.glob( + f"{self.bob_description.macros['location']}-*-*-*" + ): service_name = service.name # If service doesn't exist, file open will fail throwing exception try: @@ -206,6 +243,8 @@ def _validate_screen(self, screen_name: str): def create_screens(self): """Create the screens for each component in techui.yaml""" + + techui_name = self.techui.name if self.techui is not None else "techui.yaml" if len(self.entities) == 0: logger_.critical( "No ioc entities found. This [italic]normally[/italic]" @@ -221,7 +260,7 @@ def create_screens(self): matched = self.get_entities_for_prefix(component.prefix) if not matched: logger_.warning( - f"{self.techui.name}: prefix [bold]{component.prefix}[/bold] " + f"{techui_name}: prefix [bold]{component.prefix}[/bold] " f"for component [bold]{component_name}[/bold] matched no entities." ) continue diff --git a/src/techui_builder/jsonmap/naming.py b/src/techui_builder/jsonmap/naming.py index 4640a854..0c0d8361 100644 --- a/src/techui_builder/jsonmap/naming.py +++ b/src/techui_builder/jsonmap/naming.py @@ -1,11 +1,33 @@ """Display-name rules for ScreenNodes and duplicate-name fixing.""" +import logging +import re from collections import defaultdict from collections.abc import Mapping from pathlib import Path from techui_builder.jsonmap.nodes import ScreenNode from techui_builder.models import Component +from techui_builder.utils import _INDEX_BOB_NAME_RE + +logger_ = logging.getLogger(__name__) + + +def get_name_from_list(name: str) -> str: + """Extract the 'name' value from a string using the index_bob_name_pattern regex.""" + + macro_pairs = re.findall(_INDEX_BOB_NAME_RE, name) + + try: + if macro_pairs: + name_dict = dict(macro_pairs) + return name_dict.get("name", "") + except Exception as e: + # Log the error and the input name for debugging purposes + logger_.warning(f"Error extracting name from list: {e}. Input name: {name}") + return name # Return the original name if extraction fails + + return name def find_techui_label( @@ -17,6 +39,8 @@ def find_techui_label( if name is None: return None + name = get_name_from_list(name) + if name in components and components[name].label is not None: return components[name].label @@ -43,6 +67,8 @@ def find_techui_label( def name_or_file_stem(name: str | None, file_path: Path) -> str | None: """The name if it is non-empty, else the file name without suffixes, else None.""" + name = get_name_from_list(name) if name else None + if name: # Return name tag text as displayName return name diff --git a/src/techui_builder/main_app.py b/src/techui_builder/main_app.py index f1f7b0d3..8ce902c6 100644 --- a/src/techui_builder/main_app.py +++ b/src/techui_builder/main_app.py @@ -7,6 +7,7 @@ from techui_builder._logger import log_level from techui_builder.autofill import Autofiller +from techui_builder.bob_parser import BobParser from techui_builder.builder import Builder logger_ = logging.getLogger(__name__) @@ -20,13 +21,13 @@ def find_dirs(file_path: Path, beamline: str) -> tuple: # Get the relative path to the techui file from working dir abs_path = file_path.absolute() - logger_.debug(f"techui.yaml absolute path: {abs_path}") + logger_.debug(f"index.bob absolute path: {abs_path}") # Get the current working dir cwd = Path.cwd() logger_.debug(f"Working directory: {cwd}") - directory = beamline + directory = beamline.lower() # Get the relative path of ixx-services to techui.yaml ixx_services_dir = next( @@ -114,16 +115,9 @@ def find_index_bobs( # This is the 'build' behaviour -@app.command("build", help="Run `techui-builder build` for a given techui.yaml") +@app.command("build", help="Run `techui-builder build` for a given index.bob") def main( - filename: Annotated[Path, typer.Argument(help="The path to techui.yaml")], - bobfile: Annotated[ - Path | None, - typer.Argument( - help="Override for template bob file location. This will be used to find" - " and other template bob files in the same location with similar names." - ), - ] = None, + filename: Annotated[Path, typer.Argument(help="The path to index.bob")], loglevel: Annotated[ str, typer.Option( @@ -137,23 +131,25 @@ def main( ) -> None: """Function to run when `techui-builder build` is called.""" - gui = Builder(techui=filename) - - ixx_services_dir, synoptic_dir = find_dirs(filename, gui.conf.beamline.domain) - - index_bob_path, bob_files = find_index_bobs(bobfile, synoptic_dir) + bp = BobParser(filename) + synoptic_macros = bp.parse_bob() + ixx_services_dir, synoptic_dir = find_dirs( + filename, synoptic_macros.macros["domain"] + ) + # Need to provide builder the macros to work on generating and validating bob files. + index_bob_path, bob_files = find_index_bobs(filename, synoptic_dir) - # # Overwrite after initialised to make sure this is picked up + gui = Builder(synoptic_macros, synoptic_dir / "techui.yaml") gui._services_dir = ixx_services_dir / "services" # noqa: SLF001 gui._write_directory = synoptic_dir # noqa: SLF001 logger_.debug( f""" -Builder created for {gui.conf.beamline.domain}. -Services directory: {gui._services_dir} -Write directory: {gui._write_directory} -""", # noqa: SLF001 + Builder created for {gui.conf.beamline.domain}. + Services directory: {gui._services_dir} + Write directory: {gui._write_directory} + """, # noqa: SLF001 ) gui.setup() diff --git a/src/techui_builder/models.py b/src/techui_builder/models.py index cddf4918..523ce331 100644 --- a/src/techui_builder/models.py +++ b/src/techui_builder/models.py @@ -1,7 +1,10 @@ import logging import re +from pathlib import Path from typing import Annotated, Any +from lxml.etree import ElementTree +from lxml.objectify import ObjectifiedElement from pydantic import ( BaseModel, ConfigDict, @@ -13,7 +16,8 @@ logger_ = logging.getLogger(__name__) - +type RawMacroMap = dict[str, str] +type MacroMap = dict[str, str | list[str] | int | float] # Patterns: # long: 'bl23b' # short: 'b23', 'ixx-1' @@ -294,7 +298,17 @@ class Entity(BaseModel): "ADAravis.aravisCamera" ), ] - prefix: Annotated[str, Field(description="PV Prefix for module entity")] + prefix: Annotated[ + str, + Field( + description="Component PV Prefix", + # Make sure vscode is aware of schema validation + json_schema_extra={ + "pattern": _DLS_PREFIX_PATTERN_COMPACT, + "type": "string", + }, + ), + ] desc: Annotated[ str | None, Field(description="Optional description of module entity") ] = None @@ -363,3 +377,56 @@ class TechUiSupport(BaseModel): dict[str, SupportEntity], Field(description="The dictionary of techui-support.yaml entities"), ] + + +""" +BobParser Models +""" + + +class BobWidget(BaseModel): + """Class representation of Bob Widget""" + + name: Annotated[str, Field(description="Widget name")] + widget_type: Annotated[str, Field(description="Widget type")] + macros: Annotated[MacroMap, Field(description="Macro dictionary")] + prefix: Annotated[ + str, + Field( + description="Widget PV Name", + # Make sure vscode is aware of schema validation + json_schema_extra={ + "pattern": _DLS_PREFIX_PATTERN_VERBOSE, + "type": "string", + }, + ), + ] + element: Annotated[ + ObjectifiedElement, + Field(description="XML tree representation of the widget", repr=False), + ] + children: Annotated[ + list["BobWidget"] | None, + Field(description="Children of parent BobWidget", default_factory=list), + ] + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) + + +class BobFile(BaseModel): + """Class representation of BobFile suchs as index.bob and {component}.bob""" + + path: Annotated[Path, Field(description="Path to file")] + macros: Annotated[RawMacroMap, Field(description="Macro dictionary")] + tree: Annotated[ + ElementTree, + Field(description="XML tree representation of the file", repr=False), + ] + widgets: Annotated[ + list[BobWidget], + Field(description="Widgets in the .bob file", default_factory=list), + ] + model_config = ConfigDict( + arbitrary_types_allowed=True, + ) diff --git a/src/techui_builder/utils.py b/src/techui_builder/utils.py index cb0c50fc..6794d793 100644 --- a/src/techui_builder/utils.py +++ b/src/techui_builder/utils.py @@ -1,10 +1,13 @@ import logging +import re from enum import StrEnum from lxml import objectify from lxml.etree import Element, SubElement, tostring from lxml.objectify import ObjectifiedElement, fromstring +from techui_builder.models import RawMacroMap + logger_ = logging.getLogger(__name__) __all__ = [ @@ -14,8 +17,57 @@ "_get_actions_group", "_get_macros", "_get_nav_tabs", + "_INDEX_BOB_NAME_RE", ] +# index_bob_name_pattern = r"{?(\w+)\s*=\s*(\[.*?\]|.*?)(?=\s*,\s*\w+\s*=|\s*}$)" + +_INDEX_BOB_NAME_PATTERN_VERBOSE = r""" + (?:^{)? # optional '{' at the start of the string (the ^ only + # anchors the '{') + (?!.*\s,) # LOOKAHEAD 1 (negative): no whitespace directly before a + # comma later on the line + (?!\]*\s) # LOOKAHEAD 2 (negative): fail on whitespace here, or ']'s + # then whitespace + (?: # start of one key=value unit + (?:(?P[\w]+)) # GROUP key: the key, one or more word chars + (?:\s?=\s?) # '=' with at most one space either side + (?: # start of lookahead alternation (only one must pass) + (?=[\w\s:\-.]+) # LOOKAHEAD A (positive): a "value char" comes next + # (value char = [\w\s:\-.] = word char, whitespace, ':', + # '-' or '.') + | # OR + (?=\[?[\w\s:\-.]+(?:,\s?)?\]?) + # LOOKAHEAD B (positive): an optional '[', value chars, an + # optional ',' + one space and an optional ']' come next + # (this is the one that lets a leading '[' through) + ) # end of lookahead alternation + (?P # GROUP val: the value, either a list or a bare value + (?: # branch 1: a bracketed list, e.g. "[a, b, c]" + \[ # literal '[' + (?: + (?:[\w\s:\-.]+) # an item: one or more value chars + (?:,\s?)? # optional comma + )* # zero or more items + \] # literal ']' + ) + | # OR + (?:[\w\s:\-.]+) # branch 2: a bare value, one or more value chars + ) + ) # end of key=value unit + (?:,\s?)? # optional trailing comma (consumed, not captured) + (?:}$)? # optional '}' at the end of the string +""" + +_INDEX_BOB_NAME_RE = re.compile(_INDEX_BOB_NAME_PATTERN_VERBOSE, re.VERBOSE) + +# JSON Schema (ECMA 262) has no concept of VERBOSE mode, so derive a +# compact equivalent once at import time for use in json_schema_extra. +# (i.e. This is to handle the inline comments) +_INDEX_BOB_NAME_PATTERN_COMPACT = re.sub( + r"\s+", "", re.sub(r"#.*", "", _INDEX_BOB_NAME_PATTERN_VERBOSE) +) + class WidgetType(StrEnum): """Widget types in a .bob file.""" @@ -36,7 +88,9 @@ def read_bob(path): widgets = get_widgets(root) - return tree, widgets + macros = _get_macros(root) + + return tree, widgets, macros def get_widgets(root: ObjectifiedElement): @@ -60,7 +114,7 @@ def get_widgets(root: ObjectifiedElement): return widgets -def _get_macros(element: ObjectifiedElement): +def _get_macros(element: ObjectifiedElement) -> RawMacroMap: if hasattr(element, "macros"): macros = element.macros.getchildren() if macros is not None: diff --git a/src/techui_builder/validator.py b/src/techui_builder/validator.py index 2f5257cc..8049375d 100644 --- a/src/techui_builder/validator.py +++ b/src/techui_builder/validator.py @@ -36,7 +36,7 @@ def _check_bob(self, bob_path: Path): self.validate[bob_path.name.removesuffix(".bob")] = bob_path def _read_bob(self, path: Path): - tree, widgets = read_bob(path) + tree, widgets, _ = read_bob(path) return tree.getroot(), widgets def validate_bob( diff --git a/tests/conftest.py b/tests/conftest.py index 460072bf..6b15a10d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,11 +8,18 @@ from phoebusgen import widget as pwidget from techui_builder.autofill import Autofiller, IndexObjectDict +from techui_builder.bob_parser import BobParser from techui_builder.builder import Builder from techui_builder.generate import Generator from techui_builder.generate_jsonmap import JsonMapGenerator from techui_builder.jsonmap.nodes import ScreenNode -from techui_builder.models import Component, Entity, SupportEntity, SupportEntityScreen +from techui_builder.models import ( + BobFile, + Component, + Entity, + SupportEntity, + SupportEntityScreen, +) from techui_builder.status import GenerateStatusPvs from techui_builder.validator import Validator @@ -27,6 +34,17 @@ def tmp_t01_services(tmp_path) -> Path: return tmp_path / "t01-services" +@pytest.fixture +def bob_parser(tmp_t01_services) -> BobFile: + bob_document = BobParser(tmp_t01_services / "synoptic/index.bob").parse_bob() + return bob_document + + +@pytest.fixture +def bob_parser_object(tmp_t01_services) -> BobParser: + return BobParser(tmp_t01_services / "synoptic/index.bob") + + @pytest.fixture def tmp_test_files(tmp_path) -> Path: # Copy test files to tmp_path so they are visible to the tests @@ -36,11 +54,12 @@ def tmp_test_files(tmp_path) -> Path: @pytest.fixture -def builder(tmp_t01_services): +def builder(tmp_t01_services, bob_parser): ixx_services = tmp_t01_services + bob_document = bob_parser techui_path = ixx_services / "synoptic/techui.yaml" - b = Builder(techui_path) + b = Builder(bob_document, techui_path) b._services_dir = ixx_services / "services" b._write_directory = ixx_services / "synoptic" return b @@ -534,6 +553,26 @@ def example_xml_symbol_widget(): return widget_element +@pytest.fixture +def example_xml_symbol_widget_bob_parser(): + # You cannot set a text tag of an ObjectifiedElement, + # so we need to make an etree.Element and convert it ... + widget_element = Element("widget") + widget_element.set("type", "symbol") + widget_element.set("version", "2.0.0") + name_element = SubElement(widget_element, "name") + name_element.text = "name= new, label=test, status=[BL01T-TS-TEST-01]" + width_element = SubElement(widget_element, "width") + width_element.text = "205" + height_element = SubElement(widget_element, "height") + height_element.text = "120" + + container = Element("display") + container.append(widget_element) + # ... which requires this horror + return fromstring(tostring(container)) + + @pytest.fixture def example_xml_navtabs_widget(tmp_test_files): # You cannot set a text tag of an ObjectifiedElement, diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index adb91ed2..d16a561e 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -12,7 +12,7 @@ # Imported in to autofill from utils, so that needs to be patched @patch("techui_builder.autofill.read_bob") def test_autofiller_read_bobs(mock_read_bob: MagicMock, autofiller): - mock_read_bob.return_value = (Mock(spec=ElementTree), Mock()) + mock_read_bob.return_value = (Mock(spec=ElementTree), Mock(), Mock()) autofiller.read_bobs() diff --git a/tests/test_bob_parser.py b/tests/test_bob_parser.py new file mode 100644 index 00000000..231c7949 --- /dev/null +++ b/tests/test_bob_parser.py @@ -0,0 +1,26 @@ +from unittest.mock import MagicMock + +from lxml.etree import _ElementTree + +from techui_builder.bob_parser import BobWidget + + +def test_parse_bob(bob_parser_object, tmp_t01_services): + test_bob_path = tmp_t01_services / "synoptic/index.bob" + bob_parser_object._parse_widgets = MagicMock() + bob_file = bob_parser_object.parse_bob() + + assert bob_file is not None + assert bob_file.path == test_bob_path + assert isinstance(bob_file.tree, _ElementTree) + assert isinstance(bob_file.widgets, list) + assert isinstance(bob_file.macros, dict) + + +def test_parse_widgets(bob_parser_object, example_xml_symbol_widget_bob_parser): + widget = bob_parser_object._parse_widgets(example_xml_symbol_widget_bob_parser) + + for w in widget: + assert isinstance(w, BobWidget) + assert w.name == "new" + assert w.macros["status"] == ["BL01T-TS-TEST-01"] diff --git a/tests/test_builder.py b/tests/test_builder.py index 02a9d41f..e6fdad85 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -176,8 +176,9 @@ def test_gb_extract_services_both_yaml_files( builder._extract_entities = Mock() # overwrite to not see the bl01t service dirs - builder.conf.beamline.location = "bl01z" + builder.bob_description.macros["location"] = "bl01z" builder._services_dir = tmp_path + # Temporary files to test against (tmp_path / "bl01z-ea-temp-01").mkdir() (tmp_path / "bl01z-ea-temp-01/config").mkdir() diff --git a/tests/test_cli.py b/tests/test_cli.py index 080477d2..d368f324 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -192,6 +192,7 @@ def test_find_bob_no_bob_file_found(caplog: pytest.LogCaptureFixture): assert exc_info.value.code is None +@patch("techui_builder.main_app.BobParser") @patch("techui_builder.main_app.find_index_bobs") @patch("techui_builder.main_app.find_dirs") @patch("techui_builder.main_app.Autofiller") @@ -201,12 +202,18 @@ def test_main( mock_autofiller: MagicMock, mock_find_dirs: MagicMock, mock_find_index_bobs: MagicMock, + mock_bob_parser: MagicMock, ): mock_index_path = MagicMock(spec=Path) mock_find_dirs.return_value = MagicMock(spec=Path), MagicMock(spec=Path) mock_find_index_bobs.return_value = mock_index_path, [mock_index_path] + mock_bob_parser.return_value.parse_bob.return_value.macros.macros = { + "domain": "ixx" + } + main(mock_index_path) + mock_bob_parser.assert_called_once_with(mock_index_path) mock_find_dirs.assert_called_once() mock_find_index_bobs.assert_called_once() diff --git a/tests/test_utils.py b/tests/test_utils.py index e7d6b298..2fdb10c2 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -19,10 +19,11 @@ def test_read_bob(mock_get_widgets: MagicMock, tmp_test_files): mock_get_widgets.return_value = {"test_widget": MagicMock(spec=ObjectifiedElement)} - tree, widgets = read_bob(tmp_test_files / "index.bob") + tree, widgets, macros = read_bob(tmp_test_files / "index.bob") assert isinstance(tree, _ElementTree) assert isinstance(widgets["test_widget"], ObjectifiedElement) + assert isinstance(macros, dict) mock_get_widgets.assert_called_once() diff --git a/tests/test_validator.py b/tests/test_validator.py index de60c188..de7447ca 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -24,7 +24,7 @@ def test_validator_check_bob(validator): def test_validator_read_bob(mock_read_bob: MagicMock, validator): # We need to set the spec of the first Mock so it knows # it has a getroot() function - mock_read_bob.return_value = (Mock(spec=_ElementTree), Mock()) + mock_read_bob.return_value = (Mock(spec=_ElementTree), Mock(), Mock()) validator._read_bob(validator.bobs[0])