diff --git a/cli/src/core/enums.py b/cli/src/core/enums.py index 4e313f3..fc6dae1 100644 --- a/cli/src/core/enums.py +++ b/cli/src/core/enums.py @@ -13,3 +13,11 @@ class IOC_ENV(str, Enum): def __str__(self) -> str: return self.value + +class GuiOption(str, Enum): + NEW = "new" + EXISTING = "existing" + NONE = "none" + + def __str__(self) -> str: + return self.value diff --git a/cli/src/core/exceptions.py b/cli/src/core/exceptions.py index ba25cd0..fc62f52 100644 --- a/cli/src/core/exceptions.py +++ b/cli/src/core/exceptions.py @@ -2,3 +2,9 @@ class UVError(Exception): """Raised when running uv yields an exception""" pass + + +class InvalidCliOptions(Exception): + """Raised when running a CLI command with invalid CLI Options""" + + pass diff --git a/cli/src/core/service_creator.py b/cli/src/core/service_creator.py index 3999b89..8bd8024 100644 --- a/cli/src/core/service_creator.py +++ b/cli/src/core/service_creator.py @@ -6,12 +6,14 @@ from pathlib import Path import copier from agebd.utils import get_git_root +from core.enums import GuiOption from core.exceptions import UVError from core.git import GitRepoManager from models import ( Service, ) from models.context import ServicesContext +from models.gui_config import GuiConfig logger = logging.getLogger(__name__) @@ -30,8 +32,10 @@ class ServiceCreator: name: str, ioc_owner: str, ioc_description: str, - ui_filename: str, + gui_config: GuiConfig, ): + gui_config.assert_valid(service_registry=self.ctx.service_registry) + self.repo_manager.assert_clean_repo() original_branch = self.repo_manager.get_active_branch_name() @@ -41,9 +45,12 @@ class ServiceCreator: self.ctx.master_hla_names.add_hla_name(service.name_upper) self.ctx.iocs_overview.add_ioc(service, description=ioc_description) self.ctx.master_ioc_subs.update_content(service_name=service.name_upper) - self.ctx.service_manager_ui.update_xml( - service=service, service_name_camel=service.name_camel, ui_filename=ui_filename - ) + if gui_config.should_create_new_ui() or gui_config.should_use_existing_ui(): + self.ctx.service_manager_ui.update_xml( + service=service, + service_name_camel=service.name_camel, + ui_filename=gui_config.ui_filename, + ) branch_name = f"feature/add-service-{service.dir_name}" self.repo_manager.delete_local_branch(branch_name) @@ -53,7 +60,7 @@ class ServiceCreator: self._render_new_service_templates( service=service, ioc_owner=ioc_owner, - ui_filename=ui_filename, + gui_config=gui_config, ) self._generate_uv_lock(service=service) @@ -76,7 +83,9 @@ class ServiceCreator: self.repo_manager.switch_branch(original_branch) raise e - def _render_new_service_templates(self, service: Service, ioc_owner: str, ui_filename: str): + def _render_new_service_templates( + self, service: Service, ioc_owner: str, gui_config: GuiConfig + ): """ Render template files for a new service """ @@ -90,14 +99,15 @@ class ServiceCreator: "ioc_owner": ioc_owner, }, ) - copier.run_copy( - src_path=str(QT_TEMPLATES_DIR), - dst_path=str(self.ctx.write_paths.qt_dir), - data={ - "service_name_upper": service.name_upper, - "ui_filename": ui_filename, - }, - ) + if gui_config.should_create_new_ui(): + copier.run_copy( + src_path=str(QT_TEMPLATES_DIR), + dst_path=str(self.ctx.write_paths.qt_dir), + data={ + "service_name_upper": service.name_upper, + "ui_filename": gui_config.ui_filename, + }, + ) def _generate_uv_lock(self, service: Service): """ diff --git a/cli/src/models/gui_config.py b/cli/src/models/gui_config.py new file mode 100644 index 0000000..6b12f51 --- /dev/null +++ b/cli/src/models/gui_config.py @@ -0,0 +1,43 @@ +from typing import Optional + +from pydantic import BaseModel + +from core.enums import GuiOption +from core.exceptions import InvalidCliOptions +from models import ServiceRegistry + + +class GuiConfig(BaseModel): + option: GuiOption + raw_ui_filename: Optional[str] = None + + def should_create_new_ui(self): + return self.option == GuiOption.NEW + + def should_use_existing_ui(self): + return self.option == GuiOption.EXISTING + + @property + def ui_filename(self) -> str: + if self.raw_ui_filename is None: + raise ValueError("ui_filename is None") + + return self.raw_ui_filename + + def assert_valid(self, service_registry: ServiceRegistry): + if self.option == GuiOption.EXISTING: + if self.raw_ui_filename is None: + raise InvalidCliOptions( + f"Must specify --gui-existing-service-name when --gui-option is {GuiOption.EXISTING}" + ) + try: + service_registry.assert_service_exists(self.ui_filename) + except Exception as e: + raise InvalidCliOptions( + f"Could not find service with name {self.ui_filename} in service_registry." + ) from e + + if self.option == GuiOption.NONE and self.raw_ui_filename: + raise InvalidCliOptions( + f"Cannot specify --gui-existing-service-name when --gui-option is {GuiOption.NONE}" + ) diff --git a/cli/src/models/service_registry.py b/cli/src/models/service_registry.py index fc52547..011238e 100644 --- a/cli/src/models/service_registry.py +++ b/cli/src/models/service_registry.py @@ -20,16 +20,19 @@ class ServiceRegistry(BaseModel): return registry def get_service(self, name: str) -> Service: - for svc_data in self.services: - service = Service(**svc_data) - if name == service.name_lower: - return service - - raise ValueError(f"Could not find service with name: {name}") + return self.assert_service_exists(name=name) def get_services(self) -> list[Service]: return [Service(**svc_data) for svc_data in self.services] + def assert_service_exists(self, name: str) -> Service: + for svc_data in self.services: + service = Service(**svc_data) + if name == service.name_camel: + return service + + raise ValueError(f"Could not find service with name: {name}") + def add_service(self, name: str) -> Service: svc = Service( name=name, diff --git a/cli/src/service.py b/cli/src/service.py index cf478a2..f6035e6 100644 --- a/cli/src/service.py +++ b/cli/src/service.py @@ -2,10 +2,12 @@ import typer from agebd.utils import get_git_root, init_logging from config.paths import Paths +from core.enums import GuiOption from core.git import GitRepoManager from core.service_creator import ServiceCreator from models import ServiceRegistry from models.context import ServicesContext +from models.gui_config import GuiConfig service = typer.Typer(no_args_is_help=True) @@ -38,21 +40,32 @@ def add( "-d", help="See /docs/user/ioc/iocs_overview.md for examples", ), - ui_filename: str = typer.Option( - ..., - "--ui-filename", - "-f", - help="The in 'A_BD_.ui'; See /qt/A_BD_ServiceManager.ui for examples", + gui_option: GuiOption = typer.Option( + GuiOption.NEW, + "--gui", + "-g", + help="By default, a new GUI is created. You can specificy if a GUI already exists, " + "and this service should use that GUI, or if no GUI is needed for this service", + ), + raw_ui_filename: str | None = typer.Option( + None, + "--gui-existing-service-name", + "-s", + help="If '--gui=existing', specify the name of an existing service (in CamelCase) whose GUI this new service should use", ), ): init_logging() + if gui_option == GuiOption.NEW: + raw_ui_filename = name + gui_config = GuiConfig(option=gui_option, raw_ui_filename=raw_ui_filename) + ctx = ServicesContext.load_from_disk(Paths()) service_creator = ServiceCreator(ctx=ctx, repo_manager=GitRepoManager(REPO_ROOT)) service_creator.add_service( name=name, ioc_owner=ioc_owner, ioc_description=ioc_description, - ui_filename=ui_filename, + gui_config=gui_config, ) diff --git a/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui b/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui index 5ad06fc..808be77 100644 --- a/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui +++ b/cli/tests/fixtures/expected_output/qt/A_BD_ServiceManager.ui @@ -138,7 +138,7 @@ caLabel[statusTip="Operation"]{ - IOC=AGEBD-CPCL-DBPM3CURR,service=DBPM3CURR,name=DBPM3Current,panel=1,svc_exist=1,panelcmd=A_BD_DBPM3Current.ui;IOC=AGEBD-CPCL-TAUBPM,service=TAUBPM,name=TauBPM,panel=1,svc_exist=1,panelcmd=A_BD_TauBPM.ui;IOC=AGEBD-CPCL-TAUPCT,service=TAUPCT,name=TauPCT,panel=1,svc_exist=1,panelcmd=A_BD_TauPCT.ui;IOC=AGEBD-CPCL-TIMING,service=TIMING,name=Timing,panel=1,svc_exist=1,panelcmd=A_BD_Timing.ui;IOC=AGEBD-CPCL-SCRUBBING,service=SCRUBBING,name=Scrubbing,panel=1,svc_exist=1,panelcmd=A_BD_Scrubbing.ui;IOC=AGEBD-CPCL-INJECTIONGUARD,service=INJECTIONGUARD,name=InjectionGuard,panel=1,svc_exist=1,panelcmd=A_BD_InjectionGuard.ui;IOC=AGEBD-CPCL-POSTMORTEMLOG,service=POSTMORTEMLOG,name=PostMortemLog,panel=1,svc_exist=1,panelcmd=A_BD_PostMortemLog.ui;IOC=AGEBD-CPCL-TUNEBUMP,service=TUNEBUMP,name=TuneBump,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-PLOTS,service=PLOTS,name=Plots,panel=0,svc_exist=1,panelcmd=A_BD_Plots.ui;IOC=AGEBD-CPCL-ORBITBUMP,service=ORBITBUMP,name=OrbitBump,panel=1,svc_exist=0,panelcmd=A_BD_OrbitBump.ui;IOC=AGEBD-CPCL-TUNEFBX,service=TUNEFBX,name=Hor. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TUNEFBY,service=TUNEFBY,name=Ver. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TESTSERVICE99-X00,service=TESTSERVICE99-X00,name=TestService99-X00,panel=1,svc_exist=1,panelcmd=A_BD_TestService.ui + IOC=AGEBD-CPCL-DBPM3CURR,service=DBPM3CURR,name=DBPM3Current,panel=1,svc_exist=1,panelcmd=A_BD_DBPM3Current.ui;IOC=AGEBD-CPCL-TAUBPM,service=TAUBPM,name=TauBPM,panel=1,svc_exist=1,panelcmd=A_BD_TauBPM.ui;IOC=AGEBD-CPCL-TAUPCT,service=TAUPCT,name=TauPCT,panel=1,svc_exist=1,panelcmd=A_BD_TauPCT.ui;IOC=AGEBD-CPCL-TIMING,service=TIMING,name=Timing,panel=1,svc_exist=1,panelcmd=A_BD_Timing.ui;IOC=AGEBD-CPCL-SCRUBBING,service=SCRUBBING,name=Scrubbing,panel=1,svc_exist=1,panelcmd=A_BD_Scrubbing.ui;IOC=AGEBD-CPCL-INJECTIONGUARD,service=INJECTIONGUARD,name=InjectionGuard,panel=1,svc_exist=1,panelcmd=A_BD_InjectionGuard.ui;IOC=AGEBD-CPCL-POSTMORTEMLOG,service=POSTMORTEMLOG,name=PostMortemLog,panel=1,svc_exist=1,panelcmd=A_BD_PostMortemLog.ui;IOC=AGEBD-CPCL-TUNEBUMP,service=TUNEBUMP,name=TuneBump,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-PLOTS,service=PLOTS,name=Plots,panel=0,svc_exist=1,panelcmd=A_BD_Plots.ui;IOC=AGEBD-CPCL-ORBITBUMP,service=ORBITBUMP,name=OrbitBump,panel=1,svc_exist=0,panelcmd=A_BD_OrbitBump.ui;IOC=AGEBD-CPCL-TUNEFBX,service=TUNEFBX,name=Hor. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TUNEFBY,service=TUNEFBY,name=Ver. Tune Feedback,panel=1,svc_exist=1,panelcmd=A_BD_TuneBump.ui;IOC=AGEBD-CPCL-TESTSERVICE99-X00,service=TESTSERVICE99-X00,name=TestService99-X00,panel=1,svc_exist=1,panelcmd=A_BD_TestService99-X00.ui A_BD_ServiceManager_inc.ui diff --git a/cli/tests/fixtures/expected_output/qt/A_BD_TestService.ui b/cli/tests/fixtures/expected_output/qt/A_BD_TestService99-X00.ui similarity index 100% rename from cli/tests/fixtures/expected_output/qt/A_BD_TestService.ui rename to cli/tests/fixtures/expected_output/qt/A_BD_TestService99-X00.ui diff --git a/cli/tests/fixtures/test_repo_root/qt/A_BD_ServiceManager.ui b/cli/tests/fixtures/test_repo_root/qt/A_BD_ServiceManager.ui index beb95ef..11f85cd 100644 --- a/cli/tests/fixtures/test_repo_root/qt/A_BD_ServiceManager.ui +++ b/cli/tests/fixtures/test_repo_root/qt/A_BD_ServiceManager.ui @@ -1322,4 +1322,4 @@ caLabel[statusTip="Operation"]{ - + \ No newline at end of file diff --git a/cli/tests/tests/test_service_creator.py b/cli/tests/tests/test_service_creator.py index 34b7641..4009ce2 100644 --- a/cli/tests/tests/test_service_creator.py +++ b/cli/tests/tests/test_service_creator.py @@ -3,13 +3,17 @@ from difflib import unified_diff from pathlib import Path from unittest.mock import patch +import pytest from pytest_mock import MockerFixture from agebd.utils import get_git_root from config.paths import Paths +from core.enums import GuiOption +from core.exceptions import InvalidCliOptions from core.git import GitRepoManager from core.service_creator import ServiceCreator from models.context import ServicesContext +from models.gui_config import GuiConfig REPO_ROOT = get_git_root(__file__) TEST_REPO_ROOT = REPO_ROOT / "cli" / "tests" / "fixtures" / "test_repo_root" @@ -66,12 +70,14 @@ def test_add_new_service(tmp_path, mocker: MockerFixture): ctx = ServicesContext.load_from_disk(paths) ctx._write_paths = write_paths + service_name = "TestService99-X00" + gui_config = GuiConfig(option=GuiOption.NEW, raw_ui_filename=service_name) creator = ServiceCreator(ctx=ctx, repo_manager=mocker.Mock(spec=GitRepoManager)) creator.add_service( - name="TestService99-X00", + name=service_name, ioc_owner="test_user", ioc_description="Test Description", - ui_filename="TestService", + gui_config=gui_config, ) assert_are_dir_trees_equal(tmp_path, EXPECTED_OUTPUT_DIR, tmp_path) @@ -80,6 +86,43 @@ def test_add_new_service(tmp_path, mocker: MockerFixture): assert_all_files_staged(tmp_path, files_to_stage) +@pytest.mark.parametrize( + "gui_option,raw_ui_filename", + [ + (GuiOption.EXISTING, "DoesNotExist"), + (GuiOption.EXISTING, None), + (GuiOption.NONE, "ShouldNotBeSet"), + ], +) +def test_raise_if_gui_options_invalid(gui_option, raw_ui_filename, mocker): + paths = Paths(repo_root=TEST_REPO_ROOT) + ctx = ServicesContext.load_from_disk(paths) + + gui_config = GuiConfig(option=gui_option, raw_ui_filename=raw_ui_filename) + creator = ServiceCreator(ctx=ctx, repo_manager=mocker.Mock(spec=GitRepoManager)) + with pytest.raises(InvalidCliOptions): + creator.add_service( + name="BlaBla", + ioc_owner="test_user", + ioc_description="Test Description", + gui_config=gui_config, + ) + + +@pytest.mark.parametrize( + "gui_option,raw_ui_filename", + [ + (GuiOption.EXISTING, "Master"), + ], +) +def test_existing_gui_ok(gui_option, raw_ui_filename): + paths = Paths(repo_root=TEST_REPO_ROOT) + ctx = ServicesContext.load_from_disk(paths) + + gui_config = GuiConfig(option=gui_option, raw_ui_filename=raw_ui_filename) + gui_config.assert_valid(service_registry=ctx.service_registry) + + def assert_are_dir_trees_equal(dir1, dir2, curr_dir: Path | None = None): """ Compare two directories recursively. Files in each directory are diff --git a/config/services_registry.yml b/config/services_registry.yml index b236eb8..dfea4ae 100644 --- a/config/services_registry.yml +++ b/config/services_registry.yml @@ -6,7 +6,7 @@ services: - name: NTurns ioc_port: 50001 status: active -- name: DBPM3Current +- name: DBPM3Current ioc_port: 50002 status: active - name: TauBPM diff --git a/qt/A_BD_ServiceManager.ui b/qt/A_BD_ServiceManager.ui index beb95ef..11f85cd 100644 --- a/qt/A_BD_ServiceManager.ui +++ b/qt/A_BD_ServiceManager.ui @@ -1322,4 +1322,4 @@ caLabel[statusTip="Operation"]{ - + \ No newline at end of file