From e7038abf92562b484e499dee30051ec4aa8a7848 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Mon, 20 Jul 2026 13:05:25 +0000 Subject: [PATCH 01/10] Update t01-services example services --- .../services/bl01t-di-cam-01/config/ioc.yaml | 171 ++++++++++++++++++ .../config/fastcs.yaml | 2 +- .../config/ioc.yaml | 0 example/t01-services/synoptic/dcam1.bob | 109 +++++++++++ example/t01-services/synoptic/index.bob | 10 +- example/t01-services/synoptic/motor1.bob | 58 ++++++ example/t01-services/synoptic/techui.yaml | 14 +- 7 files changed, 356 insertions(+), 8 deletions(-) create mode 100644 example/t01-services/services/bl01t-di-cam-01/config/ioc.yaml rename example/t01-services/services/{bl01t-ea-ioc-01 => bl01t-ea-temp-01}/config/fastcs.yaml (92%) rename example/t01-services/services/{bl01t-mo-ioc-01 => bl01t-mo-motor-01}/config/ioc.yaml (100%) create mode 100644 example/t01-services/synoptic/dcam1.bob create mode 100644 example/t01-services/synoptic/motor1.bob diff --git a/example/t01-services/services/bl01t-di-cam-01/config/ioc.yaml b/example/t01-services/services/bl01t-di-cam-01/config/ioc.yaml new file mode 100644 index 00000000..0655385c --- /dev/null +++ b/example/t01-services/services/bl01t-di-cam-01/config/ioc.yaml @@ -0,0 +1,171 @@ +# yaml-language-server: $schema=/epics/ibek-defs/ioc.schema.json + +ioc_name: "{{ _global.get_env('IOC_NAME') }}" + +description: auto-generated by https://github.com/epics-containers/builder2ibek + +entities: + - type: epics.EpicsEnvSet + name: EPICS_TS_MIN_WEST + value: "0" + + - type: epics.EpicsCaMaxArrayBytes + max_bytes: 100000000 + + - type: devIocStats.iocAdminSoft + IOC: "{{ ioc_name | upper }}" + + - type: autosave.Autosave + P: "{{ ioc_name | upper }}:" + + - type: ADAravis.aravisCamera + BUFFERS: 200 + CLASS: AutoADGenICam + ID: 127.0.0.1 + P: BL01T-DI-CAM-01 + PORT: DCAM1.cam + R: ":CAM:" + + - type: ADAravis.aravisSettings + camera: DCAM1.cam + + - type: ADCore.NDROI + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.roi + QUEUE: 16 + R: ":ROI:" + TIMEOUT: 1 + + - type: ADCore.NDStats + ADDR: 0 + HIST_SIZE: 65535 + NCHANS: 20000 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.stat + QUEUE: 16 + R: ":STAT:" + TIMEOUT: 1 + XSIZE: 1292 + YSIZE: 964 + + - type: ADCore.NDStdArrays + ADDR: 0 + FTVL: UCHAR + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.roi + NELEMENTS: 1245488 + P: BL01T-DI-CAM-01 + PORT: DCAM1.arr + QUEUE: 2 + R: ":ARR:" + TIMEOUT: 1 + TYPE: Int8 + + - type: ADCore.NDProcess + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.proc + QUEUE: 16 + R: ":PROC:" + TIMEOUT: 1 + + - type: ADCore.NDOverlay + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.proc + P: BL01T-DI-CAM-01 + PORT: DCAM1.over + QUEUE: 16 + R: ":OVER:" + TIMEOUT: 1 + + - type: ADCore.NDFileTIFF + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.tiff + QUEUE: 16 + R: ":TIFF:" + TIMEOUT: 1 + + - type: ADCore.NDFileHDF5 + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.hdf + QUEUE: 16 + R: ":HDF5:" + TIMEOUT: 1 + + - type: ADCore.NDColorConvert + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.cc + QUEUE: 4 + R: ":CC:" + TIMEOUT: 1 + + - type: ADCore.NDTransform + ADDR: 0 + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.trans + R: ":TRANS:" + TIMEOUT: 1 + + - type: ffmpegServer.ffmpegStream + NDARRAY_ADDR: 0 + NDARRAY_PORT: DCAM1.cam + P: BL01T-DI-CAM-01 + PORT: DCAM1.mjpg + R: ":MJPG:" + + # Initial settings + + - type: epics.StartupCommand + command: dbLoadRecords("/epics/ioc/config/devsta.db") + + - type: epics.PostStartupCommand + command: callbackSetQueueSize(100000) + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:CAM:GC_GevSCPSPacketSize 1500 + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:CAM:GC_StrBytesPerSecond 55000000 + + # MJPG + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:MJPG:NDArrayPort DCAM1.cam + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:MJPG:EnableCallbacks Enable + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:MJPG:MAXW 1456 + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:MJPG:MAXH 1088 + + # CAM + + - type: epics.PostStartupCommand + command: dbpf BL01T-DI-CAM-01:CAM:Acquire 1 + + # Final setup + + - type: epics.PostStartupCommand + command: epicsThreadSleep(1.0) diff --git a/example/t01-services/services/bl01t-ea-ioc-01/config/fastcs.yaml b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml similarity index 92% rename from example/t01-services/services/bl01t-ea-ioc-01/config/fastcs.yaml rename to example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml index ad1541d2..790d6aed 100644 --- a/example/t01-services/services/bl01t-ea-ioc-01/config/fastcs.yaml +++ b/example/t01-services/services/bl01t-ea-temp-01/config/fastcs.yaml @@ -1,6 +1,6 @@ # yaml-language-server: $schema=schema.json controllers: - - name: BL01T-EA-TEST-01 + - name: BL01T-EA-TEMP-01 type: fastcs.TemperatureController ip_settings: ip: "localhost" diff --git a/example/t01-services/services/bl01t-mo-ioc-01/config/ioc.yaml b/example/t01-services/services/bl01t-mo-motor-01/config/ioc.yaml similarity index 100% rename from example/t01-services/services/bl01t-mo-ioc-01/config/ioc.yaml rename to example/t01-services/services/bl01t-mo-motor-01/config/ioc.yaml diff --git a/example/t01-services/synoptic/dcam1.bob b/example/t01-services/synoptic/dcam1.bob new file mode 100644 index 00000000..12797766 --- /dev/null +++ b/example/t01-services/synoptic/dcam1.bob @@ -0,0 +1,109 @@ + + + dcam1 + + Diagnostic Camera 1 + 0 + 0 + 930 + 590 + + CAM + 880 + 470 + techui-support/bob/ADAravis/ADAravis_summary.bob + +

BL01T-DI-CAM-01

+ :CAM: + + https://t01-opis.diamond.ac.uk/bl01t-di-cam-01 +
+ 0 + 0 +
+ + CAM + 100 + 40 + + CAM + + + Open Display + +

BL01T-DI-CAM-01

+ :CAM: + +
+ techui-support/bob/ADAravis/ADAravis_detail.bob + tab +
+
+ 0 + 500 +
+ + ROI + 100 + 40 + + ROI + + + Open Display + +

BL01T-DI-CAM-01

+ :ROI: + +
+ techui-support/bob/ADAravis/NDPluginROI.pvi.bob + tab +
+
+ 120 + 500 +
+ + STAT + 100 + 40 + + STAT + + + Open Display + +

BL01T-DI-CAM-01

+ :STAT: + +
+ techui-support/bob/ADAravis/NDPluginStats.pvi.bob + tab +
+
+ 240 + 500 +
+ + HDF5 + 100 + 40 + + HDF5 + + + Open Display + +

BL01T-DI-CAM-01

+ :HDF5: + +
+ techui-support/bob/ADAravis/NDFileHDF5.pvi.bob + tab +
+
+ 360 + 500 +
+
+
diff --git a/example/t01-services/synoptic/index.bob b/example/t01-services/synoptic/index.bob index 94b07187..130b1cce 100644 --- a/example/t01-services/synoptic/index.bob +++ b/example/t01-services/synoptic/index.bob @@ -1,4 +1,4 @@ - + Display @@ -21,7 +21,7 @@ MotorController - d1 + diode1 techui-support/symbols/diag-stick.svg @@ -39,7 +39,7 @@ TempController - motor + motor1 techui-support/symbols/aperture.svg @@ -64,14 +64,14 @@ Label_1 - D1 + DIODE1 250 120 1 Label_2 - Motor + Motor1 460 120 1 diff --git a/example/t01-services/synoptic/motor1.bob b/example/t01-services/synoptic/motor1.bob new file mode 100644 index 00000000..e4abe216 --- /dev/null +++ b/example/t01-services/synoptic/motor1.bob @@ -0,0 +1,58 @@ + + + motor1 + + Motor Stage + 0 + 0 + 270 + 390 + + X1 + 220 + 120 + techui-support/bob/pmac/motor_embed.bob + +

BL01T-MO-MOTOR-01

+ :X + + https://t01-opis.diamond.ac.uk/bl01t-mo-motor-01 +
+ 0 + 0 +
+ + A + 220 + 120 + techui-support/bob/pmac/motor_embed.bob + +

BL01T-MO-MOTOR-01

+ :A + + https://t01-opis.diamond.ac.uk/bl01t-mo-motor-01 +
+ 0 + 150 +
+ + pmac.GeoBrick + 100 + 40 + + pmac.GeoBrick + + + Open Display + +

BL01T-MO-BRICK-01

+
+ techui-support/bob/pmac/pmacController.bob + tab +
+
+ 0 + 300 +
+
+
diff --git a/example/t01-services/synoptic/techui.yaml b/example/t01-services/synoptic/techui.yaml index ffd9f59a..fdc94103 100644 --- a/example/t01-services/synoptic/techui.yaml +++ b/example/t01-services/synoptic/techui.yaml @@ -12,12 +12,22 @@ components: status: - BL01T-EA-FSHTR-01:FSHTR1.STAT - d1: + diode1: label: Diode 1 prefix: BL01T-DI-PHDGN-01 file: test.bob - motor: + dcam1: + label: Diagnostic Camera 1 + prefix: BL01T-DI-CAM-01 + status: + - BL01T-DI-CAM-01:CAM:DetectorState_RBV + + temp1: + label: Temperaure Controller 1 + prefix: BL01T-EA-TEMP-01 + + motor1: label: Motor Stage prefix: BL01T-MO-MOTOR-01 extras: From a9575a5f818e656ca90d07c01e230198e7e482f2 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Mon, 20 Jul 2026 13:46:14 +0000 Subject: [PATCH 02/10] Fix tests --- tests/test_builder.py | 40 +++++++++++++++++++++------------- tests/test_generate.py | 2 +- tests/test_generate_jsonmap.py | 6 ++--- tests/test_status.py | 6 ++--- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/tests/test_builder.py b/tests/test_builder.py index 653bee05..21a3ab7a 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -21,10 +21,20 @@ def test_beamline_attributes(builder, attr, expected): "index, name, label, P, R, attribute, file, extras, child_labels", [ (0, "fshtr", "Fast Shutter", "BL01T-EA-FSHTR-01", None, None, None, None, None), - (1, "d1", "Diode 1", "BL01T-DI-PHDGN-01", None, None, "test.bob", None, None), ( - 2, - "motor", + 1, + "diode1", + "Diode 1", + "BL01T-DI-PHDGN-01", + None, + None, + "test.bob", + None, + None, + ), + ( + 4, + "motor1", "Motor Stage", "BL01T-MO-MOTOR-01", None, @@ -104,8 +114,8 @@ def test_gb_extract_entities_ioc_yaml( prefix = pv.split(":", maxsplit=1)[0] builder._extract_entities( - "bl01t-mo-ioc-01", - builder._services_dir.joinpath("bl01t-mo-ioc-01/config/ioc.yaml"), + "bl01t-mo-motor-01", + builder._services_dir.joinpath("bl01t-mo-motor-01/config/ioc.yaml"), ) entity = builder.entities[prefix][index] assert entity.type == type @@ -121,8 +131,8 @@ def test_gb_extract_entities_ioc_yaml( 0, "fastcs.TemperatureController", None, - "BL01T-EA-TEST-01", - {"name": "BL01T-EA-TEST-01"}, + "BL01T-EA-TEMP-01", + {"name": "BL01T-EA-TEMP-01"}, ), ], ) @@ -136,8 +146,8 @@ def test_gb_extract_entities_fastcs_yaml( prefix = pv.split(":", maxsplit=1)[0] builder._extract_entities( - "bl01t-ea-ioc-01", - builder._services_dir.joinpath("bl01t-ea-ioc-01/config/fastcs.yaml"), + "bl01t-ea-temp-01", + builder._services_dir.joinpath("bl01t-ea-temp-01/config/fastcs.yaml"), ) entity = builder.entities[prefix][index] assert entity.type == type @@ -155,8 +165,8 @@ def test_gb_extract_services_no_yaml_files(builder, caplog, tmp_path): builder.conf.beamline.location = "bl01z" builder._services_dir = tmp_path # Temporary files to test against - (tmp_path / "bl01z-ea-ioc-01").mkdir() - (tmp_path / "bl01z-ea-ioc-01/config").mkdir() + (tmp_path / "bl01z-ea-temp-01").mkdir() + (tmp_path / "bl01z-ea-temp-01/config").mkdir() with pytest.raises(OSError) and caplog.at_level(logging.ERROR): builder._extract_services() @@ -174,10 +184,10 @@ def test_gb_extract_services_both_yaml_files(builder, caplog, tmp_path): builder.conf.beamline.location = "bl01z" builder._services_dir = tmp_path # Temporary files to test against - (tmp_path / "bl01z-ea-ioc-01").mkdir() - (tmp_path / "bl01z-ea-ioc-01/config").mkdir() - (tmp_path / "bl01z-ea-ioc-01/config/ioc.yaml").write_text("name: test") - (tmp_path / "bl01z-ea-ioc-01/config/fastcs.yaml").write_text("name: other") + (tmp_path / "bl01z-ea-temp-01").mkdir() + (tmp_path / "bl01z-ea-temp-01/config").mkdir() + (tmp_path / "bl01z-ea-temp-01/config/ioc.yaml").write_text("name: test") + (tmp_path / "bl01z-ea-temp-01/config/fastcs.yaml").write_text("name: other") with caplog.at_level(logging.CRITICAL): with pytest.raises(SystemExit): diff --git a/tests/test_generate.py b/tests/test_generate.py index af4197f2..c5002058 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -392,7 +392,7 @@ def test_generator_build_screen(generator, components): def test_build_groups_with_label(generator, components): - screen_name = "motor" + screen_name = "motor1" generator.widgets = [Mock(), Mock(), Mock()] generator._create_widgets = Mock(return_value=Mock()) generator.layout_widgets = Mock( diff --git a/tests/test_generate_jsonmap.py b/tests/test_generate_jsonmap.py index 31099c34..6840b7f8 100644 --- a/tests/test_generate_jsonmap.py +++ b/tests/test_generate_jsonmap.py @@ -348,7 +348,7 @@ def test_get_action_group_no_actions_group(caplog): def test_get_component_label(json_map_generator_with_test_files): display_name = json_map_generator_with_test_files._get_component_label( - "motor", + "motor1", None, None, ) @@ -358,7 +358,7 @@ def test_get_component_label(json_map_generator_with_test_files): def test_get_component_label_child_labels(json_map_generator_with_test_files): display_name = json_map_generator_with_test_files._get_component_label( "X", - current_component_name="motor", + current_component_name="motor1", display_name="X", ) assert display_name == "X1" @@ -369,7 +369,7 @@ def test_get_component_label_child_labels_with_name_already_pregenerated( ): display_name = json_map_generator_with_test_files._get_component_label( "X1", - current_component_name="motor", + current_component_name="motor1", display_name="X", ) assert display_name == "X1" diff --git a/tests/test_status.py b/tests/test_status.py index 678c0596..44c5c4bb 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -10,12 +10,12 @@ @patch("techui_builder.status.GenerateStatusPvs.write_status_pvs") @patch("techui_builder.status.GenerateStatusPvs.create_status_pv") -def test_status_run(mock_create, mock_write): +def test_status_run(mock_create: Mock, mock_write: Mock): mock_create.return_value = Mock() mock_write.return_value = Mock() status_run(Path("tests/t01-services/synoptic/techui.yaml").absolute()) - mock_create.assert_called_once() - mock_write.assert_called_once() + mock_create.assert_called() + mock_write.assert_called() def test_status_run_invalid_yaml(caplog): From fb6651d6e49450e5c723a87da01d159265e36258 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Mon, 20 Jul 2026 13:56:48 +0000 Subject: [PATCH 03/10] Add typehints to tests --- tests/test_autofiller.py | 14 +++++++++----- tests/test_builder.py | 16 +++++++++++----- tests/test_cli.py | 35 +++++++++++++++++++--------------- tests/test_generate.py | 4 ++-- tests/test_generate_jsonmap.py | 16 +++++++++------- tests/test_status.py | 6 +++--- tests/test_utils.py | 6 +++--- tests/test_validator.py | 4 ++-- 8 files changed, 59 insertions(+), 42 deletions(-) diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index 09da5026..4be97b8d 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from lxml.etree import ElementTree @@ -11,7 +11,7 @@ # Imported in to autofill from utils, so that needs to be patched @patch("techui_builder.autofill.read_bob") -def test_autofiller_read_bob(mock_read_bob, autofiller): +def test_autofiller_read_bob(mock_read_bob: MagicMock, autofiller): mock_read_bob.return_value = (Mock(spec=ElementTree), Mock()) autofiller.read_bob() @@ -34,7 +34,9 @@ def test_autofiller_autofill_bob(autofiller): @patch("techui_builder.generate_jsonmap.objectify.deannotate") @patch("techui_builder.generate_jsonmap.etree.ElementTree") -def test_autofiller_write_bob(mock_tree, mock_deannotate, autofiller): +def test_autofiller_write_bob( + mock_tree: MagicMock, mock_deannotate: MagicMock, autofiller +): autofiller.tree = mock_tree autofiller.write_bob(Path("tests/test_files/test_autofilled_bob.bob")) @@ -72,7 +74,7 @@ def test_autofiller_write_bob(mock_tree, mock_deannotate, autofiller): ) @patch("techui_builder.autofill._get_action_group") def test_autofiller_replace_content( - mock_get, + mock_get: MagicMock, autofiller, example_xml_related_widget, prefix, @@ -107,7 +109,9 @@ def test_autofiller_replace_content( @patch("techui_builder.autofill._get_action_group") -def test_autofiller_replace_content_no_action_group(mock_get, autofiller, caplog): +def test_autofiller_replace_content_no_action_group( + mock_get: MagicMock, autofiller, caplog: pytest.LogCaptureFixture +): # Just to only run the code we want to test autofiller.macros = ["desc"] diff --git a/tests/test_builder.py b/tests/test_builder.py index 21a3ab7a..42a90819 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -71,7 +71,7 @@ def test_component_attributes( assert component.extras == extras -def test_missing_service(builder, caplog): +def test_missing_service(builder, caplog: pytest.LogCaptureFixture): builder._extract_entities = Mock(side_effect=OSError()) builder._extract_services() for log_output in caplog.records: @@ -156,7 +156,9 @@ def test_gb_extract_entities_fastcs_yaml( assert entity.macros == macros -def test_gb_extract_services_no_yaml_files(builder, caplog, tmp_path): +def test_gb_extract_services_no_yaml_files( + builder, caplog: pytest.LogCaptureFixture, tmp_path +): # We don't want to use builder_with_setup as that calls _extract_services() # and in turn that calls _extract_entities() builder._extract_entities = Mock() @@ -175,7 +177,9 @@ def test_gb_extract_services_no_yaml_files(builder, caplog, tmp_path): assert ("No ioc.yaml or fastcs.yaml found for service:") in log_output.message -def test_gb_extract_services_both_yaml_files(builder, caplog, tmp_path): +def test_gb_extract_services_both_yaml_files( + builder, caplog: pytest.LogCaptureFixture, tmp_path +): # We don't want to use builder_with_setup as that calls _extract_services() # and in turn that calls _extract_entities() builder._extract_entities = Mock() @@ -232,7 +236,7 @@ def test_create_screens(builder_with_setup): # builder_with_setup._validate_screen.assert_called() -def test_create_screens_no_entities(builder, caplog): +def test_create_screens_no_entities(builder, caplog: pytest.LogCaptureFixture): builder.entities = [] # We only wan't to capture CRITICAL output in this test @@ -247,7 +251,9 @@ def test_create_screens_no_entities(builder, caplog): ) in log_output.message -def test_create_screens_extra_p_does_not_exist(builder_with_setup, caplog): +def test_create_screens_extra_p_does_not_exist( + builder_with_setup, caplog: pytest.LogCaptureFixture +): # We don't want to actually generate a screen builder_with_setup._generate_screen = Mock(side_effect=None) diff --git a/tests/test_cli.py b/tests/test_cli.py index 387263ea..65ec9977 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -51,7 +51,7 @@ def test_app_schema(): @patch("techui_builder.status.status_run") -def test_status_run(mock_status_run, caplog): +def test_status_run(mock_status_run: MagicMock): ClearRecords() mock_status_run.return_value = Mock() result = runner.invoke(status_app, ["example/t01-services/synoptic/techui.yaml"]) @@ -72,12 +72,12 @@ def test_status_run_output_directory(): @patch("techui_builder._logger.Logger") -def test_log_level(mock_logger): +def test_log_level(mock_logger: MagicMock): log_level("INFO") mock_logger.assert_called_once() -def test_find_dirs(caplog): +def test_find_dirs(): mock_services = MagicMock(spec=Path) mock_services.relative_to.return_value = Path("mock_rel_path") mock_parent = MagicMock(spec=Path) @@ -94,7 +94,7 @@ def test_find_dirs(caplog): assert services == Path("mock_rel_path") -def test_find_dirs_no_ixx_services_dir(caplog): +def test_find_dirs_no_ixx_services_dir(caplog: pytest.LogCaptureFixture): test_file = MagicMock(spec=Path) test_file.parents = [] @@ -108,7 +108,7 @@ def test_find_dirs_no_ixx_services_dir(caplog): assert exc_info.value.code is None -def test_find_dirs_jxx_services(caplog): +def test_find_dirs_jxx_services(caplog: pytest.LogCaptureFixture): mock_services = MagicMock(spec=Path) mock_services.relative_to.return_value = Path("mock_rel_path") mock_parent = MagicMock(spec=Path) @@ -127,7 +127,7 @@ def test_find_dirs_jxx_services(caplog): assert "ixx-services relative path:" in caplog.text -def test_find_bob(caplog): +def test_find_bob(caplog: pytest.LogCaptureFixture): bob_file = Mock(spec=Path) bob_file.exists = MagicMock(return_value=True) @@ -138,7 +138,7 @@ def test_find_bob(caplog): assert bob_file == file -def test_find_bob_bob_file_does_not_exist(caplog): +def test_find_bob_bob_file_does_not_exist(caplog: pytest.LogCaptureFixture): bad_bob_file = Path("bad_bob_file") with caplog.at_level(logging.CRITICAL) and pytest.raises(SystemExit) as exc_info: find_bob(bad_bob_file, Mock(spec=Path)) @@ -150,7 +150,7 @@ def test_find_bob_bob_file_does_not_exist(caplog): assert exc_info.value.code is None -def test_find_bob_no_bob_file_finds_default_bob_file(caplog): +def test_find_bob_no_bob_file_finds_default_bob_file(caplog: pytest.LogCaptureFixture): mock_bob_file = Path("mock_bob_file") mock_synoptic_dir = MagicMock(spec=Path) mock_synoptic_dir.glob.return_value = iter([mock_bob_file]) @@ -162,7 +162,7 @@ def test_find_bob_no_bob_file_finds_default_bob_file(caplog): assert f"bob file: {mock_bob_file}" in log_output.message -def test_find_bob_no_bob_file_found(caplog): +def test_find_bob_no_bob_file_found(caplog: pytest.LogCaptureFixture): mock_synoptic_dir = MagicMock(spec=Path) mock_synoptic_dir.glob.return_value = iter([]) @@ -183,7 +183,12 @@ def test_find_bob_no_bob_file_found(caplog): @patch("techui_builder.main_app.find_dirs") @patch("techui_builder.main_app.Autofiller") @patch("techui_builder.main_app.Builder") -def test_main(mock_builder, mock_autofiller, mock_find_dirs, mock_find_bob): +def test_main( + mock_builder: MagicMock, + mock_autofiller: MagicMock, + mock_find_dirs: MagicMock, + mock_find_bob: MagicMock, +): mock_find_dirs.return_value = Mock(), Mock() mock_path = Mock(spec=Path) main(mock_path) @@ -192,7 +197,7 @@ def test_main(mock_builder, mock_autofiller, mock_find_dirs, mock_find_bob): mock_find_bob.assert_called_once() -def test_main_json_map_no_bob_generation(caplog): +def test_main_json_map_no_bob_generation(caplog: pytest.LogCaptureFixture): runner.invoke(app, ["--generate-jsonmap"]) for log_output in caplog.records: assert ( @@ -200,14 +205,14 @@ def test_main_json_map_no_bob_generation(caplog): ) -def test_main_json_map_wrong_file(caplog): +def test_main_json_map_wrong_file(caplog: pytest.LogCaptureFixture): result = runner.invoke(generate_jsonmap_app, ["map.json"]) assert result.exit_code == 1 for log_output in caplog.records: assert "No such file or directory" in log_output.message -def test_main_json_map_generation(caplog): +def test_main_json_map_generation(caplog: pytest.LogCaptureFixture): runner.invoke( generate_jsonmap_app, [ @@ -220,7 +225,7 @@ def test_main_json_map_generation(caplog): assert "Json map generated for (from" in log_output.message -def test_main_json_map_generation_output_directory(caplog): +def test_main_json_map_generation_output_directory(caplog: pytest.LogCaptureFixture): output_dir = Path("tests/t01-services/") runner.invoke( generate_jsonmap_app, @@ -236,6 +241,6 @@ def test_main_json_map_generation_output_directory(caplog): assert "Json map generated for (from" in log_output.message -def test_main_without_techui_yaml(caplog): +def test_main_without_techui_yaml(caplog: pytest.LogCaptureFixture): result = runner.invoke(generate_jsonmap_app) assert "Missing argument 'BOB_PATH'." in result.output diff --git a/tests/test_generate.py b/tests/test_generate.py index c5002058..b2582b3c 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -106,7 +106,7 @@ def test_generator_get_group_dimensions(generator): assert width == 300 -def test_generator_create_widgets_keyerror(generator, caplog): +def test_generator_create_widgets_keyerror(generator, caplog: pytest.LogCaptureFixture): generator._get_screen_dimensions = Mock(return_value=(800, 1280)) screen_name = "test" component = Entity( @@ -438,7 +438,7 @@ def test_generator_write_screen(generator): Path("tests/test_files/test.bob").unlink() -def test_generator_write_screen_no_widgets(generator, caplog): +def test_generator_write_screen_no_widgets(generator, caplog: pytest.LogCaptureFixture): screen_name = "test" generator.screen_ = pscreen.Screen("test") generator.widgets = [] diff --git a/tests/test_generate_jsonmap.py b/tests/test_generate_jsonmap.py index 6840b7f8..fca7cf38 100644 --- a/tests/test_generate_jsonmap.py +++ b/tests/test_generate_jsonmap.py @@ -20,7 +20,7 @@ @patch("techui_builder.generate_jsonmap.Logger") -def test_log_level(mock_logger): +def test_log_level(mock_logger: MagicMock): log_level("INFO") mock_logger.assert_called_once() @@ -39,7 +39,9 @@ def test_app(): @patch("techui_builder.generate_jsonmap.yaml.safe_load") -def test_json_map_generator_techui_exception(mock_safe_load, json_map_generator): +def test_json_map_generator_techui_exception( + mock_safe_load: MagicMock, json_map_generator +): mock_safe_load.side_effect = Exception("YAML load error") with pytest.raises(Exception) as excinfo: json_map_generator.__init__(bob_path=Path("tests/test_files/test_bob.bob")) @@ -76,7 +78,7 @@ def test_write_json_map(json_map_generator): # We don't want to access the _get_action_group function in this test @patch("techui_builder.generate_jsonmap._get_action_group") def test_generate_json_map( - mock_get_action_group, + mock_get_action_group: MagicMock, json_map_generator_with_test_files, example_json_map, ): @@ -235,7 +237,7 @@ def test_fix_names_json_map_recursive(json_map_generator, example_display_names_ # We don't want to access the _get_action_group function in this test @patch("techui_builder.generate_jsonmap._get_action_group") def test_generate_json_map_get_macros( - mock_get_action_group, + mock_get_action_group: MagicMock, json_map_generator_with_test_files, example_json_map, ): @@ -279,7 +281,7 @@ def test_generate_json_map_xml_parse_error( @patch("techui_builder.generate_jsonmap._get_action_group") def test_generate_json_map_other_exception( - mock_get_action_group, + mock_get_action_group: MagicMock, json_map_generator_with_test_files, ): mock_get_action_group.side_effect = Exception("Some exception") @@ -333,7 +335,7 @@ def test_get_action_group_no_action_elements(): assert action_group is None -def test_get_action_group_no_actions_group(caplog): +def test_get_action_group_no_actions_group(caplog: pytest.LogCaptureFixture): # Use a blank xml element widget = objectify.ObjectifiedElement() # TODO: Do widgets always have a name attr, or _can_ it be empty?? @@ -403,7 +405,7 @@ def test_get_nav_tabs(example_xml_navtabs_widget): assert isinstance(tabs_widget, list) -def test_get_nav_tabs_no_tabs_group(caplog): +def test_get_nav_tabs_no_tabs_group(caplog: pytest.LogCaptureFixture): mock_navtabs = MagicMock(spec=objectify.ObjectifiedElement) mock_navtabs.name = "no_tabs" diff --git a/tests/test_status.py b/tests/test_status.py index 44c5c4bb..76fb4382 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,6 +1,6 @@ from io import StringIO from pathlib import Path -from unittest.mock import Mock, mock_open, patch +from unittest.mock import MagicMock, Mock, mock_open, patch import pytest from softioc.builder import ClearRecords, records @@ -10,7 +10,7 @@ @patch("techui_builder.status.GenerateStatusPvs.write_status_pvs") @patch("techui_builder.status.GenerateStatusPvs.create_status_pv") -def test_status_run(mock_create: Mock, mock_write: Mock): +def test_status_run(mock_create: MagicMock, mock_write: MagicMock): mock_create.return_value = Mock() mock_write.return_value = Mock() status_run(Path("tests/t01-services/synoptic/techui.yaml").absolute()) @@ -18,7 +18,7 @@ def test_status_run(mock_create: Mock, mock_write: Mock): mock_write.assert_called() -def test_status_run_invalid_yaml(caplog): +def test_status_run_invalid_yaml(caplog: pytest.LogCaptureFixture): with pytest.raises(Exception): # noqa: B017 status_run(Path("tests/invalid_techui.yaml").absolute()) assert "Error loading techui.yaml" in caplog.text diff --git a/tests/test_utils.py b/tests/test_utils.py index 13a675e1..d2ce868c 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,5 @@ from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, patch from lxml.etree import _ElementTree from lxml.objectify import Element, ObjectifiedElement @@ -8,8 +8,8 @@ @patch("techui_builder.utils.get_widgets") -def test_read_bob(mock_get_widgets): - mock_get_widgets.return_value = {"test_widget": Mock(spec=ObjectifiedElement)} +def test_read_bob(mock_get_widgets: MagicMock): + mock_get_widgets.return_value = {"test_widget": MagicMock(spec=ObjectifiedElement)} tree, widgets = read_bob(Path("tests/test_files/index.bob")) diff --git a/tests/test_validator.py b/tests/test_validator.py index 86278379..2de765e3 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,5 +1,5 @@ from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch from lxml.etree import Element, _ElementTree, tostring from lxml.objectify import fromstring @@ -22,7 +22,7 @@ def test_validator_check_bob(validator): @patch("techui_builder.validator.read_bob") -def test_validator_read_bob(mock_read_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()) From e30784f45566f3b4ff10acab96d2455317c435ae Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 08:49:39 +0000 Subject: [PATCH 04/10] Update tests to use tmp_path to avoid writing over actual files Also update test functionality --- tests/conftest.py | 25 ++++++++----- tests/test_files/test_bob_embedded.bob | 11 ++++-- tests/test_generate_jsonmap.py | 50 ++++++++++++-------------- 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3fe71d9a..445be990 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import shutil from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -118,15 +119,18 @@ def components(builder_with_test_files: Builder): @pytest.fixture -def json_map_generator(): +def json_map_generator(tmp_path): return JsonMapGenerator( - Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob")) + Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob")), + output=tmp_path, ) @pytest.fixture -def status_gen(): - return GenerateStatusPvs(Path("tests/t01-services/synoptic/techui.yaml").absolute()) +def status_gen(tmp_path): + return GenerateStatusPvs( + Path("tests/t01-services/synoptic/techui.yaml").absolute(), output=tmp_path + ) @pytest.fixture @@ -145,12 +149,15 @@ def example_json_map_root(): @pytest.fixture -def json_map_generator_with_test_files(): +def json_map_generator_with_test_files(tmp_path): + # Copy test files to tmp_path so they are visible to the tests + shutil.copytree(Path("tests/test_files/"), tmp_path / "test_files") + shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") + return JsonMapGenerator( - bob_path=Path("tests/test_files/test_bob.bob").absolute(), - techui=Path(__file__).parent.joinpath( - Path("t01-services/synoptic/techui.yaml") - ), + bob_path=tmp_path / "test_files/test_bob.bob", + techui=tmp_path / "t01-services/synoptic/techui.yaml", + output=tmp_path / "test_files", ) diff --git a/tests/test_files/test_bob_embedded.bob b/tests/test_files/test_bob_embedded.bob index f7349327..cf67cc2d 100644 --- a/tests/test_files/test_bob_embedded.bob +++ b/tests/test_files/test_bob_embedded.bob @@ -21,11 +21,16 @@ $(pv_value) $(actions)
- Embedded Display + Z + 220 + 120 motor_embed.bob + +

BL01T-MO-MOTOR-01

+ :EMBED + +
150 120 - 160 - 80
diff --git a/tests/test_generate_jsonmap.py b/tests/test_generate_jsonmap.py index fca7cf38..740c5d01 100644 --- a/tests/test_generate_jsonmap.py +++ b/tests/test_generate_jsonmap.py @@ -31,10 +31,12 @@ def test_write_json_map_no_synoptic(json_map_generator): json_map_generator.write_json_map() -def test_app(): - result = runner.invoke(app, ["tests/t01-services/synoptic/techui.yaml"]) - if Path.exists(Path("tests/t01-services/synoptic/JsonMap.json")): - os.remove("tests/t01-services/synoptic/JsonMap.json") +def test_app(tmp_path): + result = runner.invoke( + app, ["tests/t01-services/synoptic/index.bob", "--output", tmp_path] + ) + if Path.exists(Path(tmp_path / "JsonMap.json")): + os.remove(tmp_path / "JsonMap.json") assert result.exit_code == 0 @@ -48,7 +50,7 @@ def test_json_map_generator_techui_exception( assert "No such file or directory" in str(excinfo.value) -def test_write_json_map(json_map_generator): +def test_write_json_map(json_map_generator, tmp_path): test_map = JsonMap( str(Path(__file__).parent.joinpath("test_files/test_bob.bob")), None ) @@ -56,10 +58,6 @@ def test_write_json_map(json_map_generator): # We don't want cover _generate_json_map in this test json_map_generator.generate_json_map = Mock(return_value=test_map) - # Make sure opis/ dir exists - if not Path.exists(json_map_generator._write_directory): - os.mkdir(json_map_generator._write_directory) - # We don't want to access the _serialise_json_map function in this test with patch( "techui_builder.generate_jsonmap._serialise_json_map" @@ -71,9 +69,6 @@ def test_write_json_map(json_map_generator): dest_path = json_map_generator._write_directory.joinpath("JsonMap.json") assert Path.exists(dest_path) - if Path.exists(dest_path): - os.remove(dest_path) - # We don't want to access the _get_action_group function in this test @patch("techui_builder.generate_jsonmap._get_action_group") @@ -81,10 +76,9 @@ def test_generate_json_map( mock_get_action_group: MagicMock, json_map_generator_with_test_files, example_json_map, + tmp_path, ): - json_map_generator_with_test_files.bob_path = Path( - "tests/test_files/test_bob.bob" - ).absolute() + json_map_generator_with_test_files.bob_path = tmp_path / "test_files/test_bob.bob" mock_xml = objectify.Element("action") mock_xml["file"] = "test_child_bob.bob" @@ -105,7 +99,7 @@ def test_generate_json_map( def test_generate_json_map_embedded_screen( - json_map_generator_with_test_files, example_json_map + json_map_generator_with_test_files, example_json_map, tmp_path ): list_names = [ "Display", @@ -121,9 +115,9 @@ def test_generate_json_map_embedded_screen( side_effect=list_names ) - json_map_generator_with_test_files.bob_path = Path( - "tests/test_files/test_bob_embedded.bob" - ).absolute() + json_map_generator_with_test_files.bob_path = ( + tmp_path / "test_files/test_bob_embedded.bob" + ) example_json_map.file = "test_bob_embedded.bob" example_json_map.children.append( @@ -131,7 +125,7 @@ def test_generate_json_map_embedded_screen( "$(IOC)/pmacAxis.pvi.bob", display_name="Embedded Display", exists=False, - macros={"M": "$(M)", "P": "$(P)"}, + macros={"M": ":EMBED", "P": "BL01T-MO-MOTOR-01", "label": "EMBED"}, ) ) @@ -143,7 +137,7 @@ def test_generate_json_map_embedded_screen( def test_generate_json_map_nav_tabs( - json_map_generator_with_test_files, example_json_map_root + json_map_generator_with_test_files, example_json_map_root, tmp_path ): json_map_generator_with_test_files._parse_display_name = Mock( side_effect=["Display", "Tab1", "Tab2"] @@ -152,9 +146,9 @@ def test_generate_json_map_nav_tabs( side_effect=["Display", "Tab1", "Tab2"] ) - json_map_generator_with_test_files.bob_path = Path( - "tests/test_files/test_bob_navtabs.bob" - ).absolute() + json_map_generator_with_test_files.bob_path = ( + tmp_path / "test_files/test_bob_navtabs.bob" + ) example_json_map_root.file = "test_bob_navtabs.bob" example_json_map_root.children.extend( @@ -265,11 +259,11 @@ def test_generate_json_map_get_macros( def test_generate_json_map_xml_parse_error( - json_map_generator_with_test_files, + json_map_generator_with_test_files, tmp_path ): - json_map_generator_with_test_files.bob_path = Path( - "tests/test_files/test_bob_bad.bob" - ).absolute() + json_map_generator_with_test_files.bob_path = ( + tmp_path / "test_files/test_bob_bad.bob" + ) test_json_map = json_map_generator_with_test_files.generate_json_map( json_map_generator_with_test_files.bob_path, From 4c65be32d02734faf4a1118bb80a5d5614453944 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 09:47:46 +0000 Subject: [PATCH 05/10] Add status app run to launch.json --- .vscode/launch.json | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index e7d10313..b69af85a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,7 +5,7 @@ "version": "0.2.0", "configurations": [ { - "name": "Run techui-builder", + "name": "Run techui-builder build", "type": "debugpy", "request": "launch", "justMyCode": true, @@ -14,5 +14,15 @@ "console": "integratedTerminal", "args": "build ${workspaceFolder}/example/t01-services/synoptic/techui.yaml" }, + { + "name": "Run techui-builder status", + "type": "debugpy", + "request": "launch", + "justMyCode": true, + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/src/techui_builder/__main__.py", + "console": "integratedTerminal", + "args": "status ${workspaceFolder}/example/t01-services/synoptic/techui.yaml" + }, ] } From 5ef165824a008e8190f011f7d6e14e7c027dfcc8 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 12:21:07 +0000 Subject: [PATCH 06/10] Update test_cli with tmp_path --- tests/test_cli.py | 54 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 65ec9977..20118cae 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,5 @@ import logging -import os +import shutil from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -27,6 +27,7 @@ def test_app(): result = runner.invoke(app) help_result = runner.invoke(app, ["--help"]) + assert result.exit_code == 2 assert help_result.exit_code == 0 # for some reason passing '--help' outputs an extra newline at the end @@ -37,12 +38,14 @@ def test_app(): def test_app_version(): result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 assert "techui-builder version:" in result.output def test_app_schema(): result = runner.invoke(schema_app) + assert result.exit_code == 0 assert ( "\u2705 Wrote schemas/techui.schema.json\n\u2705 " @@ -54,21 +57,27 @@ def test_app_schema(): def test_status_run(mock_status_run: MagicMock): ClearRecords() mock_status_run.return_value = Mock() + result = runner.invoke(status_app, ["example/t01-services/synoptic/techui.yaml"]) + assert result.exit_code == 0 -def test_status_run_output_directory(): +def test_status_run_output_directory(tmp_path): ClearRecords() - output_dir = Path("example/t01-services/") + + # Copy t01-services to tmp_path so we don't affect the actual files + shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") + + output_dir = tmp_path / "t01-services" + result = runner.invoke( - status_app, ["-o", str(output_dir), "example/t01-services/synoptic/techui.yaml"] + status_app, + [str(tmp_path / "t01-services/synoptic/techui.yaml"), "-o", str(output_dir)], ) + assert result.exit_code == 0 - assert Path.exists(output_dir.joinpath("config/status.db")) - if Path.exists(output_dir.joinpath("config/status.db")): - os.remove(output_dir.joinpath("config/status.db")) - os.removedirs(output_dir.joinpath("config")) + assert Path.exists(output_dir / "config/status.db") @patch("techui_builder._logger.Logger") @@ -140,6 +149,7 @@ def test_find_bob(caplog: pytest.LogCaptureFixture): def test_find_bob_bob_file_does_not_exist(caplog: pytest.LogCaptureFixture): bad_bob_file = Path("bad_bob_file") + with caplog.at_level(logging.CRITICAL) and pytest.raises(SystemExit) as exc_info: find_bob(bad_bob_file, Mock(spec=Path)) @@ -199,6 +209,7 @@ def test_main( def test_main_json_map_no_bob_generation(caplog: pytest.LogCaptureFixture): runner.invoke(app, ["--generate-jsonmap"]) + for log_output in caplog.records: assert ( " Option '--generate-jsonmap' requires an argument." in log_output.message @@ -207,40 +218,49 @@ def test_main_json_map_no_bob_generation(caplog: pytest.LogCaptureFixture): def test_main_json_map_wrong_file(caplog: pytest.LogCaptureFixture): result = runner.invoke(generate_jsonmap_app, ["map.json"]) + assert result.exit_code == 1 for log_output in caplog.records: assert "No such file or directory" in log_output.message -def test_main_json_map_generation(caplog: pytest.LogCaptureFixture): +def test_main_json_map_generation(caplog: pytest.LogCaptureFixture, tmp_path): + # Copy t01-services to tmp_path so we don't affect the actual files + shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") + runner.invoke( generate_jsonmap_app, [ - "tests/t01-services/synoptic/index.bob", + str(tmp_path / "t01-services/synoptic/index.bob"), ], ) - if Path.exists(Path("tests/t01-services/synoptic/JsonMap.json")): - os.remove("tests/t01-services/synoptic/JsonMap.json") + for log_output in caplog.records: assert "Json map generated for (from" in log_output.message -def test_main_json_map_generation_output_directory(caplog: pytest.LogCaptureFixture): - output_dir = Path("tests/t01-services/") +def test_main_json_map_generation_output_directory( + caplog: pytest.LogCaptureFixture, tmp_path +): + # Copy t01-services to tmp_path so we don't affect the actual files + shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") + + output_dir = tmp_path / "t01-services" + runner.invoke( generate_jsonmap_app, [ + str(tmp_path / "t01-services/synoptic/index.bob"), "-o", str(output_dir), - "tests/t01-services/synoptic/index.bob", ], ) + assert Path.exists(output_dir.joinpath("JsonMap.json")) - os.remove(output_dir.joinpath("JsonMap.json")) for log_output in caplog.records: assert "Json map generated for (from" in log_output.message -def test_main_without_techui_yaml(caplog: pytest.LogCaptureFixture): +def test_main_json_map_generation_without_techui_yaml(caplog: pytest.LogCaptureFixture): result = runner.invoke(generate_jsonmap_app) assert "Missing argument 'BOB_PATH'." in result.output From 97dbf9a9641d47dd85a70d44bbbebfcf2df614ea Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 12:50:01 +0000 Subject: [PATCH 07/10] Add new test tmp path fixtures for t01-services and test_files --- tests/conftest.py | 72 ++++++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 29 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 445be990..960141d8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,13 +17,29 @@ @pytest.fixture -def builder(): - ixx_services = Path(__file__).parent.joinpath(Path("t01-services")) - techui_path = ixx_services.joinpath("synoptic/techui.yaml") +def tmp_t01_services(tmp_path) -> Path: + # Copy test files to tmp_path so they are visible to the tests + shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") + + return tmp_path / "t01-services" + + +@pytest.fixture +def tmp_test_files(tmp_path) -> Path: + # Copy test files to tmp_path so they are visible to the tests + shutil.copytree(Path("tests/test_files/"), tmp_path / "test_files") + + return tmp_path / "test_files" + + +@pytest.fixture +def builder(tmp_t01_services): + ixx_services = tmp_t01_services + techui_path = ixx_services / "synoptic/techui.yaml" b = Builder(techui_path) - b._services_dir = ixx_services.joinpath("services") - b._write_directory = ixx_services.joinpath("synoptic") + b._services_dir = ixx_services / "services" + b._write_directory = ixx_services / "synoptic" return b @@ -107,8 +123,8 @@ def builder_with_setup(builder: Builder, techui_support): @pytest.fixture -def builder_with_test_files(builder: Builder): - builder._write_directory = Path("tests/test_files/").absolute() +def builder_with_test_files(builder: Builder, tmp_test_files): + builder._write_directory = tmp_test_files return builder @@ -119,24 +135,26 @@ def components(builder_with_test_files: Builder): @pytest.fixture -def json_map_generator(tmp_path): +def json_map_generator(tmp_t01_services): return JsonMapGenerator( - Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob")), - output=tmp_path, + tmp_t01_services / "synoptic/index.bob", + tmp_t01_services / "synoptic/techui.yaml", + output=tmp_t01_services / "synoptic", ) @pytest.fixture -def status_gen(tmp_path): +def status_gen(tmp_t01_services): return GenerateStatusPvs( - Path("tests/t01-services/synoptic/techui.yaml").absolute(), output=tmp_path + tmp_t01_services / "synoptic/techui.yaml", + output=tmp_t01_services / "synoptic/config", ) @pytest.fixture -def test_files(): - screen_path = Path("tests/test_files/test_bob.bob").absolute() - dest_path = Path("tests/test_files/").absolute() +def test_files(tmp_test_files): + screen_path = tmp_test_files / "test_bob.bob" + dest_path = tmp_test_files return screen_path, dest_path @@ -149,15 +167,11 @@ def example_json_map_root(): @pytest.fixture -def json_map_generator_with_test_files(tmp_path): - # Copy test files to tmp_path so they are visible to the tests - shutil.copytree(Path("tests/test_files/"), tmp_path / "test_files") - shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") - +def json_map_generator_with_test_files(tmp_t01_services, tmp_test_files): return JsonMapGenerator( - bob_path=tmp_path / "test_files/test_bob.bob", - techui=tmp_path / "t01-services/synoptic/techui.yaml", - output=tmp_path / "test_files", + bob_path=tmp_test_files / "test_bob.bob", + techui=tmp_t01_services / "synoptic/techui.yaml", + output=tmp_test_files, ) @@ -223,8 +237,8 @@ def example_display_names_json(): @pytest.fixture -def generator(techui_support): - synoptic_dir = Path(__file__).parent.joinpath(Path("t01-services/synoptic")) +def generator(techui_support, tmp_t01_services): + synoptic_dir = tmp_t01_services / "synoptic" techui_support_path = synoptic_dir.joinpath("techui-support") g = Generator(synoptic_dir, "test_url", techui_support_path, techui_support) @@ -233,8 +247,8 @@ def generator(techui_support): @pytest.fixture -def autofiller(): - index_bob = Path(__file__).parent.joinpath(Path("t01-services/synoptic/index.bob")) +def autofiller(tmp_t01_services): + index_bob = tmp_t01_services / "synoptic/index.bob" a = Autofiller(index_bob, {"test_widget": MagicMock(spec=Component)}) @@ -242,8 +256,8 @@ def autofiller(): @pytest.fixture -def validator(): - test_bobs = [Path("tests/test_files/motor-edited.bob")] +def validator(tmp_test_files): + test_bobs = [tmp_test_files / "motor_edited.bob"] v = Validator(test_bobs) return v From 2712484f6f9cb5b5158305f800489b0f73b52e30 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 12:50:24 +0000 Subject: [PATCH 08/10] Rename motor-edited.bob to motor_edited.bob --- tests/test_files/{motor-edited.bob => motor_edited.bob} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/test_files/{motor-edited.bob => motor_edited.bob} (100%) diff --git a/tests/test_files/motor-edited.bob b/tests/test_files/motor_edited.bob similarity index 100% rename from tests/test_files/motor-edited.bob rename to tests/test_files/motor_edited.bob From bab1b89eb8c1497b5558272a5d5614cdf83c386d Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 14:42:30 +0000 Subject: [PATCH 09/10] Fix all tests to use tmp_path fixtures Also update status app --- src/techui_builder/status.py | 16 +++---- tests/conftest.py | 18 ++++---- tests/test_autofiller.py | 7 ++- tests/test_builder.py | 4 +- tests/test_cli.py | 34 ++++++--------- tests/test_files/motor_edited.bob | 2 +- tests/test_generate.py | 71 ++++++++++++++++++------------- tests/test_generate_jsonmap.py | 60 +++++++++++++------------- tests/test_status.py | 14 +++--- tests/test_utils.py | 5 +-- tests/test_validator.py | 11 +++-- 11 files changed, 120 insertions(+), 122 deletions(-) diff --git a/src/techui_builder/status.py b/src/techui_builder/status.py index 4847673e..88e6a82a 100644 --- a/src/techui_builder/status.py +++ b/src/techui_builder/status.py @@ -28,11 +28,13 @@ class GenerateStatusPvs: output: Path | None = field(default=None) def __post_init__(self): - # Determine the directory to write the json map file to. + # Determine the directory to write the status db file to. # By default, this looks at the location of the techui file, but can # be overwritten using the --output flag - self._write_directory: Path = ( - self.output if self.output is not None else self.techui_path.parent + self._conf_dir: Path = ( + self.output + if self.output is not None + else self.techui_path.parent / "config" ) try: @@ -70,13 +72,11 @@ def create_status_pv(self, prefix: str, inputs: list[str]): self.status_pvs[prefix] = status_pv def write_status_pvs(self): - conf_dir = self._write_directory.joinpath("config") - # Create the config/ dir if it doesn't exist - if not conf_dir.exists(): - os.mkdir(conf_dir) + if not self._conf_dir.exists(): + os.mkdir(self._conf_dir) - with open(conf_dir.joinpath("status.db"), "w") as f: + with open(self._conf_dir / "status.db", "w") as f: # Add a header explaining the file is autogenerated f.write("#" * 51 + "\n") f.write( diff --git a/tests/conftest.py b/tests/conftest.py index 960141d8..5e22ec61 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -147,7 +147,7 @@ def json_map_generator(tmp_t01_services): def status_gen(tmp_t01_services): return GenerateStatusPvs( tmp_t01_services / "synoptic/techui.yaml", - output=tmp_t01_services / "synoptic/config", + output=tmp_t01_services / "synoptic/", ) @@ -264,7 +264,7 @@ def validator(tmp_test_files): @pytest.fixture -def example_xml_embedded_widget(): +def example_xml_embedded_widget(tmp_test_files): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -278,7 +278,7 @@ def example_xml_embedded_widget(): height_element = SubElement(widget_element, "height") height_element.text = "120" file_element = SubElement(widget_element, "file") - file_element.text = "tests/test-files/motor_embed.bob" + file_element.text = str(tmp_test_files / "motor_embed.bob") macros_element = SubElement(widget_element, "macros") macro_element_1 = SubElement(macros_element, "macro1") macro_element_1.text = "test_macro_1" @@ -290,7 +290,7 @@ def example_xml_embedded_widget(): @pytest.fixture -def example_xml_related_widget(): +def example_xml_related_widget(tmp_t01_services): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -308,8 +308,8 @@ def example_xml_related_widget(): 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" + file_element.text = str( + tmp_t01_services / "synoptic/techui-support/bob/pmac/motor.bob" ) desc_element = SubElement(action_element, "description") desc_element.text = "placeholder description" @@ -344,7 +344,7 @@ def example_xml_symbol_widget(): @pytest.fixture -def example_xml_navtabs_widget(): +def example_xml_navtabs_widget(tmp_test_files): # You cannot set a text tag of an ObjectifiedElement, # so we need to make an etree.Element and convert it ... @@ -364,7 +364,7 @@ def example_xml_navtabs_widget(): name_element_1 = SubElement(tab_element_1, "name") name_element_1.text = "tab1" file_element_1 = SubElement(tab_element_1, "file") - file_element_1.text = "tests/test-files/motor_embed.bob" + file_element_1.text = str(tmp_test_files / "motor_embed.bob") macros_element_1 = SubElement(tab_element_1, "macros") macro_element_1 = SubElement(macros_element_1, "macro1") macro_element_1.text = "test_macro_1" @@ -372,7 +372,7 @@ def example_xml_navtabs_widget(): name_element_2 = SubElement(tab_element_2, "name") name_element_2.text = "tab2" file_element_2 = SubElement(tab_element_2, "file") - file_element_2.text = "tests/test-files/motor_embed.bob" + file_element_2.text = str(tmp_test_files / "motor_embed.bob") macros_element_2 = SubElement(tab_element_2, "macros") macro_element_2 = SubElement(macros_element_2, "macro2") macro_element_2.text = "test_macro_2" diff --git a/tests/test_autofiller.py b/tests/test_autofiller.py index 4be97b8d..46d62745 100644 --- a/tests/test_autofiller.py +++ b/tests/test_autofiller.py @@ -1,5 +1,4 @@ import logging -from pathlib import Path from unittest.mock import MagicMock, Mock, patch import pytest @@ -35,15 +34,15 @@ def test_autofiller_autofill_bob(autofiller): @patch("techui_builder.generate_jsonmap.objectify.deannotate") @patch("techui_builder.generate_jsonmap.etree.ElementTree") def test_autofiller_write_bob( - mock_tree: MagicMock, mock_deannotate: MagicMock, autofiller + mock_tree: MagicMock, mock_deannotate: MagicMock, autofiller, tmp_test_files ): autofiller.tree = mock_tree - autofiller.write_bob(Path("tests/test_files/test_autofilled_bob.bob")) + autofiller.write_bob(tmp_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"), + tmp_test_files / "test_autofilled_bob.bob", pretty_print=True, encoding="utf-8", xml_declaration=True, diff --git a/tests/test_builder.py b/tests/test_builder.py index 42a90819..bc9766f1 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -115,7 +115,7 @@ def test_gb_extract_entities_ioc_yaml( builder._extract_entities( "bl01t-mo-motor-01", - builder._services_dir.joinpath("bl01t-mo-motor-01/config/ioc.yaml"), + builder._services_dir / "bl01t-mo-motor-01/config/ioc.yaml", ) entity = builder.entities[prefix][index] assert entity.type == type @@ -147,7 +147,7 @@ def test_gb_extract_entities_fastcs_yaml( builder._extract_entities( "bl01t-ea-temp-01", - builder._services_dir.joinpath("bl01t-ea-temp-01/config/fastcs.yaml"), + builder._services_dir / "bl01t-ea-temp-01/config/fastcs.yaml", ) entity = builder.entities[prefix][index] assert entity.type == type diff --git a/tests/test_cli.py b/tests/test_cli.py index 20118cae..199749eb 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,5 +1,4 @@ import logging -import shutil from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -54,30 +53,27 @@ def test_app_schema(): @patch("techui_builder.status.status_run") -def test_status_run(mock_status_run: MagicMock): +def test_status_run(mock_status_run: MagicMock, tmp_t01_services): ClearRecords() mock_status_run.return_value = Mock() - result = runner.invoke(status_app, ["example/t01-services/synoptic/techui.yaml"]) + result = runner.invoke(status_app, [str(tmp_t01_services / "synoptic/techui.yaml")]) assert result.exit_code == 0 -def test_status_run_output_directory(tmp_path): +def test_status_run_output_directory(tmp_t01_services): ClearRecords() - # Copy t01-services to tmp_path so we don't affect the actual files - shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") - - output_dir = tmp_path / "t01-services" + output_dir = tmp_t01_services result = runner.invoke( status_app, - [str(tmp_path / "t01-services/synoptic/techui.yaml"), "-o", str(output_dir)], + [str(tmp_t01_services / "synoptic/techui.yaml"), "-o", str(output_dir)], ) assert result.exit_code == 0 - assert Path.exists(output_dir / "config/status.db") + assert Path.exists(output_dir / "synoptic/config/status.db") @patch("techui_builder._logger.Logger") @@ -224,14 +220,11 @@ def test_main_json_map_wrong_file(caplog: pytest.LogCaptureFixture): assert "No such file or directory" in log_output.message -def test_main_json_map_generation(caplog: pytest.LogCaptureFixture, tmp_path): - # Copy t01-services to tmp_path so we don't affect the actual files - shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") - +def test_main_json_map_generation(caplog: pytest.LogCaptureFixture, tmp_t01_services): runner.invoke( generate_jsonmap_app, [ - str(tmp_path / "t01-services/synoptic/index.bob"), + str(tmp_t01_services / "synoptic/index.bob"), ], ) @@ -240,23 +233,20 @@ def test_main_json_map_generation(caplog: pytest.LogCaptureFixture, tmp_path): def test_main_json_map_generation_output_directory( - caplog: pytest.LogCaptureFixture, tmp_path + caplog: pytest.LogCaptureFixture, tmp_t01_services ): - # Copy t01-services to tmp_path so we don't affect the actual files - shutil.copytree(Path(__file__).parent / "t01-services", tmp_path / "t01-services") - - output_dir = tmp_path / "t01-services" + output_dir = tmp_t01_services runner.invoke( generate_jsonmap_app, [ - str(tmp_path / "t01-services/synoptic/index.bob"), + str(tmp_t01_services / "synoptic/index.bob"), "-o", str(output_dir), ], ) - assert Path.exists(output_dir.joinpath("JsonMap.json")) + assert Path.exists(output_dir / "JsonMap.json") for log_output in caplog.records: assert "Json map generated for (from" in log_output.message diff --git a/tests/test_files/motor_edited.bob b/tests/test_files/motor_edited.bob index 06900ea4..e5c37b75 100644 --- a/tests/test_files/motor_edited.bob +++ b/tests/test_files/motor_edited.bob @@ -12,7 +12,7 @@ X 205 120 - tests/test-files/motor_embed.bob + motor_embed.bob

BL01T-MO-MOTOR-01

X diff --git a/tests/test_generate.py b/tests/test_generate.py index b2582b3c..6b497b9a 100644 --- a/tests/test_generate.py +++ b/tests/test_generate.py @@ -24,15 +24,15 @@ def y(self, val: int): self._y = val -def test_generator_get_screen_dimensions_good(generator): - test_embedded_screen = "tests/test_files/motor_embed.bob" +def test_generator_get_screen_dimensions_good(generator, tmp_test_files): + test_embedded_screen = tmp_test_files / "motor_embed.bob" x, y = generator._get_screen_dimensions(test_embedded_screen) assert x == 120 assert y == 205 -def test_generator_get_screen_dimensions_default(generator): - test_embedded_screen = "tests/test_files/motor_bad.bob" +def test_generator_get_screen_dimensions_default(generator, tmp_test_files): + test_embedded_screen = tmp_test_files / "motor_bad.bob" x, y = generator._get_screen_dimensions(test_embedded_screen) assert x == 100 assert y == 100 @@ -46,8 +46,8 @@ def test_generator_get_widget_dimensions_good(generator): assert width == 205 -def test_generator_get_widget_dimensions_default(generator): - widget_bad = Path("tests/test_files/widget_bad.xml") +def test_generator_get_widget_dimensions_default(generator, tmp_test_files): + widget_bad = tmp_test_files / "widget_bad.xml" with open(widget_bad) as f: xml_content_bad = f.read() @@ -57,8 +57,10 @@ def test_generator_get_widget_dimensions_default(generator): assert width == 100 -def test_generator_get_widget_dimensions_default_attribute_error(generator): - widget_bad = Path("tests/test_files/widget_bad_2.xml") +def test_generator_get_widget_dimensions_default_attribute_error( + generator, tmp_test_files +): + widget_bad = tmp_test_files / "widget_bad_2.xml" with open(widget_bad) as f: xml_content_bad = f.read() @@ -76,8 +78,8 @@ def test_generator_get_widget_position(generator): assert y == 0 -def test_generator_get_widget_position_default(generator): - widget_bad = Path("tests/test_files/widget_bad.xml") +def test_generator_get_widget_position_default(generator, tmp_test_files): + widget_bad = tmp_test_files / "widget_bad.xml" with open(widget_bad) as f: xml_content_bad = f.read() @@ -87,8 +89,10 @@ def test_generator_get_widget_position_default(generator): assert y == 100 -def test_generator_get_widget_position_default_attribute_error(generator): - widget_bad = Path("tests/test_files/widget_bad_2.xml") +def test_generator_get_widget_position_default_attribute_error( + generator, tmp_test_files +): + widget_bad = tmp_test_files / "widget_bad_2.xml" with open(widget_bad) as f: xml_content_bad = f.read() @@ -126,7 +130,9 @@ def test_generator_create_widgets_keyerror(generator, caplog: pytest.LogCaptureF ) -def test_generator_create_widgets_embedded(generator, example_pgen_embedded_widget): +def test_generator_create_widgets_embedded( + generator, example_pgen_embedded_widget, tmp_test_files +): generator._allocate_widget = Mock(return_value=example_pgen_embedded_widget) screen_name = "test" @@ -142,7 +148,7 @@ def test_generator_create_widgets_embedded(generator, example_pgen_embedded_widg name=screen_name, component=component, ) - control_widget = Path("tests/test_files/widget.xml") + control_widget = tmp_test_files / "widget.xml" with open(control_widget) as f: xml_content = f.read() @@ -207,7 +213,7 @@ def test_generator_update_macros_suffix_with_child_labels(generator): assert updated_macros["label"] == child_label -def test_generator_allocate_widget(generator): +def test_generator_allocate_widget(generator, tmp_test_files): generator._update_macros = Mock( return_value=("CAM", {"P": "BL01T-DI-IOC-01", "R": ":CAM:", "label": "CAM"}) ) @@ -226,7 +232,7 @@ def test_generator_allocate_widget(generator): macros={"P": "BL01T-DI-IOC-01", "R": ":CAM:"}, ) widget = generator._allocate_widget(scrn_mapping, component) - control_widget = Path("tests/test_files/widget.xml") + control_widget = tmp_test_files / "widget.xml" with open(control_widget) as f: xml_content = f.read() @@ -234,7 +240,7 @@ def test_generator_allocate_widget(generator): assert str(widget) == xml_content -def test_generator_allocate_widget_with_remote_screens(generator): +def test_generator_allocate_widget_with_remote_screens(generator, tmp_test_files): generator._update_macros = Mock( return_value=("CAM", {"P": "BL01T-DI-IOC-01", "R": ":CAM:", "label": "CAM"}) ) @@ -250,7 +256,7 @@ def test_generator_allocate_widget_with_remote_screens(generator): 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") + control_widget = tmp_test_files / "widget_url_screen.xml" with open(control_widget) as f: xml_content = f.read() @@ -258,7 +264,7 @@ def test_generator_allocate_widget_with_remote_screens(generator): assert str(widget) == xml_content -def test_generator_allocate_widget_with_custom_suffix(generator): +def test_generator_allocate_widget_with_custom_suffix(generator, tmp_test_files): generator._update_macros = Mock(return_value=("CAM", {"P": "BL01T-DI-IOC-01"})) generator._get_screen_dimensions = Mock(return_value=(40, 100)) @@ -275,7 +281,7 @@ def test_generator_allocate_widget_with_custom_suffix(generator): macros={"P": "BL01T-DI-IOC-01"}, ) widget = generator._allocate_widget(scrn_mapping, component) - control_widget = Path("tests/test_files/widget_custom_suffix.xml") + control_widget = tmp_test_files / "widget_custom_suffix.xml" with open(control_widget) as f: xml_content = f.read() @@ -283,7 +289,9 @@ def test_generator_allocate_widget_with_custom_suffix(generator): assert str(widget) == xml_content -def test_generator_create_widgets_related(generator, example_pgen_related_widget): +def test_generator_create_widgets_related( + generator, example_pgen_related_widget, tmp_test_files +): generator._allocate_widget = Mock(return_value=example_pgen_related_widget) generator._get_screen_dimensions = Mock(return_value=(800, 1280)) @@ -300,14 +308,15 @@ def test_generator_create_widgets_related(generator, example_pgen_related_widget component=component, ) - control_widget = Path("tests/test_files/widget_related.xml") + control_widget = tmp_test_files / "widget_related.xml" with open(control_widget) as f: xml_content = f.read() + assert str(widgets[0]) == xml_content # def test_generator_create_widgets_related_no_suffix( -# generator, example_pgen_related_widget +# generator, example_pgen_related_widget, tmp_test_files # ): # generator._allocate_widget = Mock(return_value=example_pgen_related_widget) # generator._get_screen_dimensions = Mock(return_value=(800, 1280)) @@ -325,7 +334,7 @@ def test_generator_create_widgets_related(generator, example_pgen_related_widget # component=component, # ) -# control_widget = Path("tests/test_files/widget_related_no_suffix.xml") +# control_widget = tmp_test_files / "widget_related_no_suffix.xml" # with open(control_widget) as f: # xml_content = f.read() @@ -429,18 +438,20 @@ def test_build_groups(generator, components): assert xml.xpath("//name")[0] == "test" -def test_generator_write_screen(generator): +def test_generator_write_screen(generator, tmp_test_files): screen_name = "test" generator.screen_ = pscreen.Screen("test") generator.widgets = [Mock(), Mock()] - generator.write_screen(screen_name, Path("tests/test_files/")) - assert Path("tests/test_files/test.bob").exists() - Path("tests/test_files/test.bob").unlink() + generator.write_screen(screen_name, tmp_test_files) + + assert Path.exists(tmp_test_files / "test.bob") -def test_generator_write_screen_no_widgets(generator, caplog: pytest.LogCaptureFixture): +def test_generator_write_screen_no_widgets( + generator, caplog: pytest.LogCaptureFixture, tmp_test_files +): screen_name = "test" generator.screen_ = pscreen.Screen("test") generator.widgets = [] - generator.write_screen(screen_name, Path("tests/test_files/")) + generator.write_screen(screen_name, tmp_test_files) assert "Could not write screen: test as no widgets were available" in caplog.text diff --git a/tests/test_generate_jsonmap.py b/tests/test_generate_jsonmap.py index 740c5d01..923d0aad 100644 --- a/tests/test_generate_jsonmap.py +++ b/tests/test_generate_jsonmap.py @@ -1,5 +1,4 @@ import logging -import os from pathlib import Path from unittest.mock import MagicMock, Mock, patch @@ -31,29 +30,32 @@ def test_write_json_map_no_synoptic(json_map_generator): json_map_generator.write_json_map() -def test_app(tmp_path): +def test_app(tmp_t01_services): result = runner.invoke( - app, ["tests/t01-services/synoptic/index.bob", "--output", tmp_path] + app, + [ + str(tmp_t01_services / "synoptic/index.bob"), + "--output", + str(tmp_t01_services / "synoptic"), + ], ) - if Path.exists(Path(tmp_path / "JsonMap.json")): - os.remove(tmp_path / "JsonMap.json") + assert result.exit_code == 0 @patch("techui_builder.generate_jsonmap.yaml.safe_load") def test_json_map_generator_techui_exception( - mock_safe_load: MagicMock, json_map_generator + mock_safe_load: MagicMock, json_map_generator, tmp_test_files ): mock_safe_load.side_effect = Exception("YAML load error") with pytest.raises(Exception) as excinfo: - json_map_generator.__init__(bob_path=Path("tests/test_files/test_bob.bob")) + json_map_generator.__init__(bob_path=tmp_test_files / "test_bob.bob") + assert "No such file or directory" in str(excinfo.value) -def test_write_json_map(json_map_generator, tmp_path): - test_map = JsonMap( - str(Path(__file__).parent.joinpath("test_files/test_bob.bob")), None - ) +def test_write_json_map(json_map_generator, tmp_test_files): + test_map = JsonMap(str(tmp_test_files / "test_bob.bob"), None) # We don't want cover _generate_json_map in this test json_map_generator.generate_json_map = Mock(return_value=test_map) @@ -66,7 +68,8 @@ def test_write_json_map(json_map_generator, tmp_path): json_map_generator.write_json_map() - dest_path = json_map_generator._write_directory.joinpath("JsonMap.json") + dest_path = json_map_generator._write_directory / "JsonMap.json" + assert Path.exists(dest_path) @@ -76,10 +79,7 @@ def test_generate_json_map( mock_get_action_group: MagicMock, json_map_generator_with_test_files, example_json_map, - tmp_path, ): - json_map_generator_with_test_files.bob_path = tmp_path / "test_files/test_bob.bob" - mock_xml = objectify.Element("action") mock_xml["file"] = "test_child_bob.bob" mock_get_action_group.return_value = mock_xml @@ -99,7 +99,7 @@ def test_generate_json_map( def test_generate_json_map_embedded_screen( - json_map_generator_with_test_files, example_json_map, tmp_path + json_map_generator_with_test_files, example_json_map, tmp_test_files ): list_names = [ "Display", @@ -116,7 +116,7 @@ def test_generate_json_map_embedded_screen( ) json_map_generator_with_test_files.bob_path = ( - tmp_path / "test_files/test_bob_embedded.bob" + tmp_test_files / "test_bob_embedded.bob" ) example_json_map.file = "test_bob_embedded.bob" @@ -137,7 +137,7 @@ def test_generate_json_map_embedded_screen( def test_generate_json_map_nav_tabs( - json_map_generator_with_test_files, example_json_map_root, tmp_path + json_map_generator_with_test_files, example_json_map_root, tmp_test_files ): json_map_generator_with_test_files._parse_display_name = Mock( side_effect=["Display", "Tab1", "Tab2"] @@ -147,7 +147,7 @@ def test_generate_json_map_nav_tabs( ) json_map_generator_with_test_files.bob_path = ( - tmp_path / "test_files/test_bob_navtabs.bob" + tmp_test_files / "test_bob_navtabs.bob" ) example_json_map_root.file = "test_bob_navtabs.bob" @@ -189,12 +189,12 @@ def test_parse_display_name_returns_none(json_map_generator): assert display_name is None -def test_fix_names_json_map_recursive(json_map_generator, example_display_names_json): +def test_fix_names_json_map_recursive( + json_map_generator, example_display_names_json, tmp_test_files +): """Test duplicate names are enumerated correctly for all children""" - test_display_names_json = JsonMap( - str(Path(__file__).parent.joinpath("test_files/test_bob.bob")), None - ) + test_display_names_json = JsonMap(str(tmp_test_files / "test_bob.bob"), None) test_display_names_json_det1 = JsonMap( "test_child_bob.bob", "Detector", macros={"P": "PV-DET-01"}, exists=False @@ -259,11 +259,9 @@ def test_generate_json_map_get_macros( def test_generate_json_map_xml_parse_error( - json_map_generator_with_test_files, tmp_path + json_map_generator_with_test_files, tmp_test_files ): - json_map_generator_with_test_files.bob_path = ( - tmp_path / "test_files/test_bob_bad.bob" - ) + json_map_generator_with_test_files.bob_path = tmp_test_files / "test_bob_bad.bob" test_json_map = json_map_generator_with_test_files.generate_json_map( json_map_generator_with_test_files.bob_path, @@ -306,8 +304,8 @@ def test_serialise_json_map(example_json_map): } -def test_get_action_group(): - test_bob = objectify.parse("tests/test_files/test_bob.bob") +def test_get_action_group(tmp_test_files): + test_bob = objectify.parse(tmp_test_files / "test_bob.bob") widget = test_bob.find(".//widget") assert widget is not None @@ -316,8 +314,8 @@ def test_get_action_group(): assert action_group is not None -def test_get_action_group_no_action_elements(): - test_bob = objectify.parse("tests/test_files/test_bob.bob") +def test_get_action_group_no_action_elements(tmp_test_files): + test_bob = objectify.parse(tmp_test_files / "test_bob.bob") widget = test_bob.find(".//widget") assert widget is not None diff --git a/tests/test_status.py b/tests/test_status.py index 76fb4382..eea38536 100644 --- a/tests/test_status.py +++ b/tests/test_status.py @@ -1,5 +1,4 @@ from io import StringIO -from pathlib import Path from unittest.mock import MagicMock, Mock, mock_open, patch import pytest @@ -10,17 +9,20 @@ @patch("techui_builder.status.GenerateStatusPvs.write_status_pvs") @patch("techui_builder.status.GenerateStatusPvs.create_status_pv") -def test_status_run(mock_create: MagicMock, mock_write: MagicMock): +def test_status_run(mock_create: MagicMock, mock_write: MagicMock, tmp_t01_services): mock_create.return_value = Mock() mock_write.return_value = Mock() - status_run(Path("tests/t01-services/synoptic/techui.yaml").absolute()) + + status_run(tmp_t01_services / "synoptic/techui.yaml") + mock_create.assert_called() mock_write.assert_called() -def test_status_run_invalid_yaml(caplog: pytest.LogCaptureFixture): +def test_status_run_invalid_yaml(caplog: pytest.LogCaptureFixture, tmp_test_files): with pytest.raises(Exception): # noqa: B017 - status_run(Path("tests/invalid_techui.yaml").absolute()) + status_run(tmp_test_files / "invalid_techui.yaml") + assert "Error loading techui.yaml" in caplog.text @@ -87,7 +89,7 @@ def test_status_write_status_pvs(status_gen): # Check open() was called with the correct args m.assert_called_once_with( - Path(status_gen._write_directory.joinpath("config/status.db")), + status_gen._conf_dir / "status.db", "w", ) mock_print.assert_called_once() diff --git a/tests/test_utils.py b/tests/test_utils.py index d2ce868c..fb2565c9 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,4 +1,3 @@ -from pathlib import Path from unittest.mock import MagicMock, patch from lxml.etree import _ElementTree @@ -8,10 +7,10 @@ @patch("techui_builder.utils.get_widgets") -def test_read_bob(mock_get_widgets: MagicMock): +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(Path("tests/test_files/index.bob")) + tree, widgets = read_bob(tmp_test_files / "index.bob") assert isinstance(tree, _ElementTree) assert isinstance(widgets["test_widget"], ObjectifiedElement) diff --git a/tests/test_validator.py b/tests/test_validator.py index 2de765e3..de60c188 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,4 +1,3 @@ -from pathlib import Path from unittest.mock import MagicMock, Mock, patch from lxml.etree import Element, _ElementTree, tostring @@ -18,7 +17,7 @@ def test_validator_check_bob(validator): validator._check_bob(validator.bobs[0]) assert len(validator.validate.keys()) > 0 - assert list(validator.validate.keys())[0] == "motor-edited" + assert list(validator.validate.keys())[0] == "motor_edited" @patch("techui_builder.validator.read_bob") @@ -31,7 +30,7 @@ def test_validator_read_bob(mock_read_bob: MagicMock, validator): # TODO: Clean up this test... (make fixture for mock xml?) -def test_validator_validate_bob(validator, example_xml_embedded_widget): +def test_validator_validate_bob(validator, example_xml_embedded_widget, tmp_test_files): # 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") @@ -46,10 +45,10 @@ def test_validator_validate_bob(validator, example_xml_embedded_widget): {"motor": (mock_element)}, ) ) - validator.validate = {"motor-edited": Path("tests/test_files/motor-edited.bob")} + validator.validate = {"motor_edited": tmp_test_files / "motor_edited.bob"} test_pwidget = EmbeddedDisplay( "motor", - "tests/test-files/motor_embed.bob", + tmp_test_files / "motor_embed.bob", 0, 0, 205, @@ -57,4 +56,4 @@ def test_validator_validate_bob(validator, example_xml_embedded_widget): ) test_pwidget.macro("macro1", "test_macro_1") - validator.validate_bob("motor-edited", "motor", [test_pwidget]) + validator.validate_bob("motor_edited", "motor", [test_pwidget]) From 9d86be5cbc08aa6a7b2d0fcc53d81fcb2f8aa31f Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Tue, 21 Jul 2026 14:43:19 +0000 Subject: [PATCH 10/10] Add missing t01-services example files These were always deleted when running tests --- example/t01-services/synoptic/JsonMap.json | 101 ++++++++ .../t01-services/synoptic/config/status.db | 41 ++++ example/t01-services/synoptic/index.bob | 215 ++++++++++++------ 3 files changed, 289 insertions(+), 68 deletions(-) create mode 100644 example/t01-services/synoptic/JsonMap.json create mode 100644 example/t01-services/synoptic/config/status.db diff --git a/example/t01-services/synoptic/JsonMap.json b/example/t01-services/synoptic/JsonMap.json new file mode 100644 index 00000000..7b3732ae --- /dev/null +++ b/example/t01-services/synoptic/JsonMap.json @@ -0,0 +1,101 @@ +{ + "file": "index.bob", + "children": [ + { + "file": "dcam1.bob", + "children": [ + { + "file": "ProfileCursorGraphs.bob", + "macros": { + "P": "BL01T-DI-CAM-01", + "R": ":CAM:", + "label": "CAM", + "IOC": "https://t01-opis.diamond.ac.uk/bl01t-di-cam-01" + }, + "displayName": "CAM" + }, + { + "file": "techui-support/bob/ADAravis/ADAravis_detail.bob", + "macros": { + "P": "BL01T-DI-CAM-01", + "R": ":CAM:", + "label": "CAM" + }, + "displayName": "ADAravis Camera" + }, + { + "file": "techui-support/bob/ADAravis/NDPluginROI.pvi.bob", + "macros": { + "P": "BL01T-DI-CAM-01", + "R": ":ROI:", + "label": "ROI" + }, + "displayName": "NDPluginROI" + }, + { + "file": "techui-support/bob/ADAravis/NDPluginStats.pvi.bob", + "macros": { + "P": "BL01T-DI-CAM-01", + "R": ":STAT:", + "label": "STAT" + }, + "displayName": "NDPluginStats" + }, + { + "file": "techui-support/bob/ADAravis/NDFileHDF5.pvi.bob", + "macros": { + "P": "BL01T-DI-CAM-01", + "R": ":HDF5:", + "label": "HDF5" + }, + "displayName": "NDFileHDF5" + } + ], + "displayName": "Diagnostic Camera 1" + }, + { + "file": "test.bob", + "exists": false, + "displayName": "Diode 1" + }, + { + "file": "temp1.bob", + "exists": false, + "displayName": "Temperaure Controller 1" + }, + { + "file": "motor1.bob", + "children": [ + { + "file": "$(IOC)/pmacAxis.pvi.bob", + "macros": { + "M": ":X", + "P": "BL01T-MO-MOTOR-01", + "label": "X1", + "IOC": "https://t01-opis.diamond.ac.uk/bl01t-mo-motor-01" + }, + "displayName": "X1" + }, + { + "file": "$(IOC)/pmacAxis.pvi.bob", + "macros": { + "M": ":A", + "P": "BL01T-MO-MOTOR-01", + "label": "A", + "IOC": "https://t01-opis.diamond.ac.uk/bl01t-mo-motor-01" + }, + "displayName": "A" + }, + { + "file": "techui-support/bob/pmac/pmacController.bob", + "macros": { + "P": "BL01T-MO-BRICK-01" + }, + "displayName": "pmacController" + } + ], + "displayName": "Motor Stage" + } + ], + "displayName": "Display" +} diff --git a/example/t01-services/synoptic/config/status.db b/example/t01-services/synoptic/config/status.db new file mode 100644 index 00000000..b1bf0dae --- /dev/null +++ b/example/t01-services/synoptic/config/status.db @@ -0,0 +1,41 @@ +################################################### +## THIS FILE HAS BEEN AUTOGENERATED; DO NOT EDIT ## +################################################### + +record(calc, "BL01T-EA-FSHTR-01:STA") +{ + field(ACKT, "NO") + field(CALC, "(A|B|C|D|E|F|G|H|I|J|K|L)>0?1:0") + field(INPA, "BL01T-EA-FSHTR-01:FSHTR1.STAT") + field(INPB, "") + field(INPC, "") + field(INPD, "") + field(INPE, "") + field(INPF, "") + field(INPG, "") + field(INPH, "") + field(INPI, "") + field(INPJ, "") + field(INPK, "") + field(INPL, "") + field(SCAN, "1 second") +} + +record(calc, "BL01T-DI-CAM-01:STA") +{ + field(ACKT, "NO") + field(CALC, "(A|B|C|D|E|F|G|H|I|J|K|L)>0?1:0") + field(INPA, "BL01T-DI-CAM-01:CAM:DetectorState_RBV") + field(INPB, "") + field(INPC, "") + field(INPD, "") + field(INPE, "") + field(INPF, "") + field(INPG, "") + field(INPH, "") + field(INPI, "") + field(INPJ, "") + field(INPK, "") + field(INPL, "") + field(SCAN, "1 second") +} diff --git a/example/t01-services/synoptic/index.bob b/example/t01-services/synoptic/index.bob index 130b1cce..e6cd7aca 100644 --- a/example/t01-services/synoptic/index.bob +++ b/example/t01-services/synoptic/index.bob @@ -1,79 +1,158 @@ + Display - - Detector - fshtr - - techui-support/symbols/shutter 1.svg - - 60 - 150 - 50 - 50 - - - Detector.bob - replace - Open Display - - - - - MotorController - diode1 - - techui-support/symbols/diag-stick.svg - - 280 - 150 - 50 - 50 - - - MotorController.bob - replace - Open Display - - - - - TempController - motor1 - - techui-support/symbols/aperture.svg - - 490 - 150 - 50 - 50 - - - TempController.bob - replace - Open Display - - + + dcam1_Group + 250 + 120 + 100 + 80 + + true + + dcam1 + + techui-support/symbols/camera.svg + + 30 + 30 + 50 + 50 + + + Diagnostic Camera 1 + dcam1.bob + replace + + + $(pv_name) +$(pv_value) +$(actions) + BL01T-DI-CAM-01:STA + true + + + dcam1_L:abel + DCAM1 + 1 + $(pv_name) +$(pv_value) +$(actions) + - - Label - FSHTR + + diode1_Group 30 - 130 - 1 - - - Label_1 - DIODE1 - 250 120 - 1 + 100 + 80 + + true + + diode1 + + techui-support/symbols/oscilloscope.svg + + 30 + 30 + 50 + 50 + + + Diode 1 + test.bob + replace + + + $(pv_name) +$(pv_value) +$(actions) + BL01T-DI-PHDGN-01:STA + true + + + diode1_Label + DIODE1 + 1 + - - Label_2 - Motor1 + + temp1_Group 460 120 - 1 + 100 + 80 + + true + + temp1 + BL01T-EA-TEMP-01:STA + + techui-support/symbols/cryocooler.svg + + 30 + 30 + 50 + 50 + + + Temperaure Controller 1 + temp1.bob + replace + + + $(pv_name) +$(pv_value) +$(actions) + true + + + temp1_label + temp1 + 1 + $(pv_name) +$(pv_value) +$(actions) + + + + temp1_Group_1 + 650 + 120 + 100 + 80 + + true + + motor1 + BL01T-MO-MOTOR-01:STA + + techui-support/symbols/translation-stage.svg + + 30 + 30 + 50 + 50 + + + Motor Stage + motor1.bob + replace + + + $(pv_name) +$(pv_value) +$(actions) + true + + + motor1_label + motor1 + 1 + $(pv_name) +$(pv_value) +$(actions) +