From 405583da35d167095bd4d7b178caf542cc3f40f2 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Thu, 11 Dec 2025 15:57:53 +0000 Subject: [PATCH 1/7] Add tests for Autofiller --- tests/conftest.py | 10 ++++++++ tests/test_autofiller.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 tests/test_autofiller.py diff --git a/tests/conftest.py b/tests/conftest.py index 92e088b..2786306 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import pytest +from techui_builder.autofill import Autofiller from techui_builder.builder import Builder, JsonMap from techui_builder.generate import Generator from techui_builder.validator import Validator @@ -62,6 +63,15 @@ def generator(): return g +@pytest.fixture +def autofiller(): + index_bob = Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob")) + + a = Autofiller(index_bob) + + return a + + @pytest.fixture def validator(): test_bobs = [Path("tests/test_files/motor-edited.bob")] diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py new file mode 100644 index 0000000..44f969f --- /dev/null +++ b/tests/test_autofiller.py @@ -0,0 +1,50 @@ +from pathlib import Path +from unittest.mock import Mock, patch + +from lxml.etree import ElementTree +from lxml.objectify import Element + +from techui_builder.models import Component + + +def test_autofiller_read_bob(autofiller): + # Imported in to autofill from utils, so that needs to be patched + with patch("techui_builder.autofill.read_bob") as mock_read_bob: + mock_read_bob.return_value = (Mock(spec=ElementTree), Mock()) + + autofiller.read_bob() + + mock_read_bob.assert_called() + + +def test_autofiller_autofill_bob(autofiller): + autofiller.replace_content = Mock() + # This mess of a Mock represents a basic Builder object with a components dict + mock_builder = Mock(conf=Mock(components={"test_widget": Mock(spec=Component)})) + + mock_widget = Element("widget") + + autofiller.widgets = {"test_widget": mock_widget} + + autofiller.autofill_bob(mock_builder) + + autofiller.replace_content.assert_called() + assert mock_widget.find("run_actions_on_mouse_click") == "true" + + +def test_autofiller_write_bob(autofiller): + with ( + patch("techui_builder.builder.etree.ElementTree") as mock_tree, + patch("techui_builder.builder.objectify.deannotate") as mock_deannotate, + ): + autofiller.tree = mock_tree + + autofiller.write_bob(Path("tests/test_files/test_autofilled_bob.bob")) + + mock_deannotate.assert_called_once() + mock_tree.write.assert_called_once_with( + Path("tests/test_files/test_autofilled_bob.bob"), + pretty_print=True, + encoding="utf-8", + xml_declaration=True, + ) From c994690cda6908a5ccda671acf9629dd5353bbda Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 12 Dec 2025 14:58:02 +0000 Subject: [PATCH 2/7] Add example widgets to conftest.py --- tests/conftest.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 2786306..f640fa3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock, patch import pytest +from lxml.etree import Element, SubElement, tostring +from lxml.objectify import fromstring from techui_builder.autofill import Autofiller from techui_builder.builder import Builder, JsonMap @@ -78,3 +80,59 @@ def validator(): v = Validator(test_bobs) return v + + +@pytest.fixture +def example_embedded_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") + widget_element.set("type", "embedded") + widget_element.set("version", "2.0.0") + name_element = SubElement(widget_element, "name") + name_element.text = "motor" + width_element = SubElement(widget_element, "width") + width_element.text = "205" + height_element = SubElement(widget_element, "height") + height_element.text = "120" + file_element = SubElement(widget_element, "file") + file_element.text = ( + "example/t01-services/synoptic/techui-support/bob/pmac/motor_embed.bob" + ) + + # ... which requires this horror + widget_element = fromstring(tostring(widget_element)) + + return widget_element + + +@pytest.fixture +def example_related_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") + widget_element.set("type", "action_button") + widget_element.set("version", "2.0.0") + name_element = SubElement(widget_element, "name") + name_element.text = "motor" + width_element = SubElement(widget_element, "width") + width_element.text = "205" + height_element = SubElement(widget_element, "height") + height_element.text = "120" + + actions_element = SubElement(widget_element, "actions") + action_element = SubElement(actions_element, "action") + action_element.set("type", "open_display") + file_element = SubElement(action_element, "file") + file_element.text = ( + "example/t01-services/synoptic/techui-support/bob/pmac/motor.bob" + ) + desc_element = SubElement(action_element, "description") + desc_element.text = "placeholder description" + + # ... which requires this horror + widget_element = fromstring(tostring(widget_element)) + + return widget_element From b63769bf7462b1d2f6bf808abd378217b396678e Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 12 Dec 2025 14:58:22 +0000 Subject: [PATCH 3/7] Add test for replace_content --- tests/test_autofiller.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index 44f969f..bbe9317 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -1,6 +1,7 @@ from pathlib import Path from unittest.mock import Mock, patch +import pytest from lxml.etree import ElementTree from lxml.objectify import Element @@ -48,3 +49,40 @@ def test_autofiller_write_bob(autofiller): encoding="utf-8", xml_declaration=True, ) + + +@pytest.mark.parametrize( + "prefix, description, filename, expected_desc, expected_file", + [ + ("TEST_1", None, None, "test_component", "test_component.bob"), + ("TEST_2", "test_desc", "test_file.bob", "test_desc", "test_file.bob"), + ], +) +def test_autofiller_replace_content( + autofiller, + example_related_widget, + prefix, + description, + filename, + expected_desc, + expected_file, +): + with patch("techui_builder.autofill._get_action_group") as mock_get: + mock_get.return_value = example_related_widget.actions.action + + mock_component = Mock( + spec=Component, + prefix=prefix, + desc=description, + file=filename, + ) + + autofiller.replace_content( + example_related_widget, + "test_component", + mock_component, + ) + + assert example_related_widget.pv_name == f"{prefix}:DEVSTA" + assert example_related_widget.actions.action.description.text == expected_desc + assert example_related_widget.actions.action.file.text == expected_file From ad8603df4e926fc0f0fdd9a9d40c19abca595a33 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 12 Dec 2025 14:58:44 +0000 Subject: [PATCH 4/7] Tidy validator tests with new fixtures --- tests/test_validator.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/tests/test_validator.py b/tests/test_validator.py index 0ee4da1..3936c43 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,7 +1,7 @@ from pathlib import Path from unittest.mock import Mock, patch -from lxml.etree import Element, SubElement, _ElementTree, tostring +from lxml.etree import Element, _ElementTree, tostring from lxml.objectify import fromstring from phoebusgen.widget import EmbeddedDisplay @@ -31,21 +31,11 @@ def test_validator_read_bob(validator): # TODO: Clean up this test... (make fixture for mock xml?) -def test_validator_validate_bob(validator): +def test_validator_validate_bob(validator, example_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_widget_element = SubElement(mock_root_element, "widget") - mock_name_element = SubElement(mock_widget_element, "name") - mock_name_element.text = "motor" - mock_width_element = SubElement(mock_widget_element, "width") - mock_width_element.text = "205" - mock_height_element = SubElement(mock_widget_element, "height") - mock_height_element.text = "120" - mock_file_element = SubElement(mock_widget_element, "file") - mock_file_element.text = ( - "example/t01-services/synoptic/techui_supportbob/pmac/motor_embed.bob" - ) + mock_root_element.append(example_embedded_widget) # ... which requires this horror mock_element = fromstring(tostring(mock_root_element)) # mock_element = ObjectifiedElement(mock_widget_element) @@ -59,7 +49,7 @@ def test_validator_validate_bob(validator): validator.validate = {"motor-edited": Path("tests/test_files/motor-edited.bob")} test_pwidget = EmbeddedDisplay( "motor", - "example/t01-services/synoptic/techui_supportbob/pmac/motor_embed.bob", + "example/t01-services/synoptic/techui-support/bob/pmac/motor_embed.bob", 0, 0, 205, From 451fc0b11b21d2043ab4694d91ffb1a19c51df38 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Fri, 12 Dec 2025 15:33:18 +0000 Subject: [PATCH 5/7] More tests for autofiller --- tests/test_autofiller.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index bbe9317..564ee94 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -1,3 +1,4 @@ +import logging from pathlib import Path from unittest.mock import Mock, patch @@ -86,3 +87,37 @@ def test_autofiller_replace_content( assert example_related_widget.pv_name == f"{prefix}:DEVSTA" assert example_related_widget.actions.action.description.text == expected_desc assert example_related_widget.actions.action.file.text == expected_file + + +def test_autofiller_replace_content_no_action_group(autofiller, caplog): + # Just to only run the code we want to test + autofiller.macros = ["desc"] + + with patch("techui_builder.autofill._get_action_group") as mock_get: + # Simulate no action group found + mock_get.return_value = None + + mock_component = Mock( + spec=Component, + desc="description", + ) + + with caplog.at_level(logging.DEBUG): + autofiller.replace_content(None, "", mock_component) + + for log_output in caplog.records: + assert "Skipping replace_content for" in log_output.message + + +def test_autofiller_replace_content_unsupported_macro(autofiller): + autofiller.macros = ["bad_macro"] + + mock_component = Mock( + spec=Component, + bad_macro="bad_macro", + ) + + with pytest.raises(ValueError) as e: + autofiller.replace_content(None, "", mock_component) + + assert e == "The provided macro type is not supported." From 5fb7ae7144b29e22992d8b00844556a9017a6f46 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Mon, 15 Dec 2025 09:09:18 +0000 Subject: [PATCH 6/7] Add tests for utils functions Added example symbol widget fixture to conftest.py --- tests/conftest.py | 20 ++++++++++++++++++++ tests/test_utils.py | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/test_utils.py diff --git a/tests/conftest.py b/tests/conftest.py index f640fa3..fe0a905 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -136,3 +136,23 @@ def example_related_widget(): widget_element = fromstring(tostring(widget_element)) return widget_element + + +@pytest.fixture +def example_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") + widget_element.set("type", "symbol") + widget_element.set("version", "2.0.0") + name_element = SubElement(widget_element, "name") + name_element.text = "motor" + width_element = SubElement(widget_element, "width") + width_element.text = "205" + height_element = SubElement(widget_element, "height") + height_element.text = "120" + + # ... which requires this horror + widget_element = fromstring(tostring(widget_element)) + + return widget_element diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..4fe352c --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,27 @@ +from pathlib import Path +from unittest.mock import Mock, patch + +from lxml.etree import _ElementTree +from lxml.objectify import Element, ObjectifiedElement + +from techui_builder.utils import get_widgets, read_bob + + +def test_read_bob(): + with patch("techui_builder.utils.get_widgets") as mock_get_widgets: + mock_get_widgets.return_value = {"test_widget": Mock(spec=ObjectifiedElement)} + + tree, widgets = read_bob(Path("tests/test_files/index.bob")) + + assert isinstance(tree, _ElementTree) + assert isinstance(widgets["test_widget"], ObjectifiedElement) + mock_get_widgets.assert_called_once() + + +def test_get_widgets(example_symbol_widget): + test_root = Element("root") + test_root.append(example_symbol_widget) + + widgets = get_widgets(test_root) + + assert "motor" in widgets.keys() From 06a41db7ff8fac8bed681730da46d3f3545d8c60 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Mon, 15 Dec 2025 11:58:42 +0000 Subject: [PATCH 7/7] Add a few missing tests for Builder --- tests/test_builder.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_builder.py b/tests/test_builder.py index 6c6374b..9002f1c 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -5,6 +5,7 @@ import pytest from lxml import objectify +from phoebusgen.widget import ActionButton, Group from techui_builder.builder import ( JsonMap, @@ -108,6 +109,30 @@ def test_gb_extract_entities(builder, index, type, desc, P, M, R): # noqa: N803 assert entity.R == R +def test_builder_generate_screen(builder_with_setup): + # with ( + # patch("techui_builder.builder.Generator.build_screen") as mock_build_screen, + # patch("techui_builder.builder.Generator.write_screen") as mock_write_screen, + # ): + builder_with_setup.generator.build_screen = Mock() + builder_with_setup.generator.write_screen = Mock() + + builder_with_setup._generate_screen("TEST") + + builder_with_setup.generator.build_screen.assert_called_once() + builder_with_setup.generator.write_screen.assert_called_once() + + +def test_builder_validate_screen(builder_with_setup): + builder_with_setup.validator.validate_bob = Mock() + builder_with_setup.generator.widgets = [Mock(spec=ActionButton)] + builder_with_setup.generator.group = Mock(spec=Group, name="TEST") + + builder_with_setup._validate_screen("TEST") + + builder_with_setup.validator.validate_bob.assert_called_once() + + def test_create_screens(builder_with_setup): # We don't want to access Generator in this test builder_with_setup._generate_screen = Mock()