diff --git a/doc/source/changelog/928.added.md b/doc/source/changelog/928.added.md new file mode 100644 index 0000000000..84ecf7c441 --- /dev/null +++ b/doc/source/changelog/928.added.md @@ -0,0 +1 @@ +Unify application start by using a launcher diff --git a/src/ansys/stk/extensions/launcher.py b/src/ansys/stk/extensions/launcher.py new file mode 100644 index 0000000000..2cb0f2bc7f --- /dev/null +++ b/src/ansys/stk/extensions/launcher.py @@ -0,0 +1,145 @@ +# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Module for unifying the launching of STK.""" + +from enum import Enum, unique +from typing import TypeAlias + +from ansys.stk.core.stkdesktop import STKDesktop, STKDesktopApplication +from ansys.stk.core.stkengine import STKEngine, STKEngineApplication +from ansys.stk.core.stkruntime import STKRuntime, STKRuntimeApplication + +STKApplication: TypeAlias = STKEngineApplication | STKDesktopApplication | STKRuntimeApplication +"""Type alias for supported STK application instances.""" + +# TODO: use StrEnum when minimum Python version is 3.11 # noqa: TD003 +@unique +class STKApplicationType(str, Enum): + """Supported STK application types.""" + + ENGINE = "engine" + """Launch STK without the graphical user interface.""" + + DESKTOP = "desktop" + """Launch STK with the graphical user interface.""" + + RUNTIME = "runtime" + """Launch STK out-of-process using gRPC.""" + + +def launch_stk(mode: STKApplicationType | str, **configuration) -> STKApplication: + """Launch STK based on the configured application type. + + Parameters + ---------- + mode : STKApplicationType | str + The mode in which to launch STK. + **configuration : dict, default: None + Supported configuration for the selected launch mode. For allowable + keyword arguments, see the corresponding methods for each mode: + + * For ``ENGINE`` mode, see the :func:`ansys.stk.core.stkengine.STKEngine.start_application` method. + * For ``DESKTOP`` mode, see the :func:`ansys.stk.core.stkdesktop.STKDesktop.start_application` method. + * For ``RUNTIME`` mode, see the :func:`ansys.stk.core.stkruntime.STKRuntime.start_application` method. + + Returns + ------- + STKApplication + An instance of the launched STK application. + + Raises + ------ + ValueError + If an unsupported application type is configured. + """ + # Normalize mode to STKApplicationType + if isinstance(mode, str): + try: + mode = STKApplicationType(mode) + except ValueError as exc: + raise ValueError(f"Unsupported STK application type: {mode!r}") from exc + if not isinstance(mode, STKApplicationType): + raise ValueError( + "Mode must be an instance of STKApplicationType or a valid string value." + ) + + # Dispatch to the appropriate launcher + if mode == STKApplicationType.ENGINE: + stk = STKEngine + elif mode == STKApplicationType.DESKTOP: + stk = STKDesktop + elif mode == STKApplicationType.RUNTIME: + stk = STKRuntime + + return stk.start_application(**configuration) + + +def attach_to_application(mode: STKApplicationType | str, **configuration) -> STKApplication: + """Attach to an existing STK instance based on the configured application type. + + Parameters + ---------- + mode : STKApplicationType | str + The mode in which to attach to STK. ENGINE mode is not supported. + **configuration : dict, default: None + Supported configuration for the selected attach mode. For allowable + keyword arguments, see the corresponding methods for each mode: + + * For ``DESKTOP`` mode, see the :func:`ansys.stk.core.stkdesktop.STKDesktop.attach_to_application` method. + * For ``RUNTIME`` mode, see the :func:`ansys.stk.core.stkruntime.STKRuntime.attach_to_application` method. + + Returns + ------- + STKApplication + An instance of the attached STK application. + + Raises + ------ + ValueError + If an unsupported application type is configured or if ENGINE mode is specified. + NotImplementedError + If the specified mode does not support attaching to an existing instance. + """ + # Normalize mode to STKApplicationType + if isinstance(mode, str): + try: + mode = STKApplicationType(mode) + except ValueError as exc: + raise ValueError(f"Unsupported STK application type: {mode!r}") from exc + if not isinstance(mode, STKApplicationType): + raise ValueError( + "Mode must be an instance of STKApplicationType or a valid string value." + ) + + if mode is STKApplicationType.ENGINE: + raise NotImplementedError( + "STKEngine mode does not support attaching to an existing instance." + ) + + # Dispatch to the appropriate attach method + if mode == STKApplicationType.DESKTOP: + stk = STKDesktop + elif mode == STKApplicationType.RUNTIME: + stk = STKRuntime + + return stk.attach_to_application(**configuration) diff --git a/tests/extensions/test_launcher.py b/tests/extensions/test_launcher.py new file mode 100644 index 0000000000..9a05116990 --- /dev/null +++ b/tests/extensions/test_launcher.py @@ -0,0 +1,228 @@ +# Copyright (C) 2022 - 2026 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests for the launcher extension.""" + +from unittest.mock import MagicMock, patch + +import pytest + +from ansys.stk.extensions.launcher import ( + STKApplicationType, + attach_to_application, + launch_stk, +) + +@patch("ansys.stk.extensions.launcher.STKEngine") +def test_launch_stk_engine_with_string(mock_engine): + """Test launching STK in ENGINE mode using string mode.""" + mock_app = MagicMock() + mock_engine.start_application.return_value = mock_app + + result = launch_stk(mode="engine", no_graphics=True) + + mock_engine.start_application.assert_called_once_with(no_graphics=True) + assert result == mock_app + + +@patch("ansys.stk.extensions.launcher.STKEngine") +def test_launch_stk_engine_with_enum(mock_engine): + """Test launching STK in ENGINE mode using enum mode.""" + mock_app = MagicMock() + mock_engine.start_application.return_value = mock_app + + result = launch_stk(mode=STKApplicationType.ENGINE, no_graphics=True) + + mock_engine.start_application.assert_called_once_with(no_graphics=True) + assert result == mock_app + + +@patch("ansys.stk.extensions.launcher.STKDesktop") +def test_launch_stk_desktop_with_string(mock_desktop): + """Test launching STK in DESKTOP mode using string mode.""" + mock_app = MagicMock() + mock_desktop.start_application.return_value = mock_app + + result = launch_stk(mode="desktop", visible=False, user_control=False) + + mock_desktop.start_application.assert_called_once_with(visible=False, user_control=False) + assert result == mock_app + + +@patch("ansys.stk.extensions.launcher.STKDesktop") +def test_launch_stk_desktop_with_enum(mock_desktop): + """Test launching STK in DESKTOP mode using enum mode.""" + mock_app = MagicMock() + mock_desktop.start_application.return_value = mock_app + + result = launch_stk(mode=STKApplicationType.DESKTOP, visible=False, user_control=False) + + mock_desktop.start_application.assert_called_once_with(visible=False, user_control=False) + assert result == mock_app + + +def test_launch_stk_runtime_with_string(): + """Test launching STK in RUNTIME mode using string mode.""" + result = launch_stk(mode="runtime", grpc_host="localhost", grpc_port=50051) + result.shutdown() + + +def test_launch_stk_runtime_with_enum(): + """Test launching STK in RUNTIME mode using enum mode.""" + result = launch_stk(mode=STKApplicationType.RUNTIME, grpc_host="127.0.0.1", grpc_port=40704) + result.shutdown() + + +def test_launch_stk_invalid_string_mode(): + """Test that invalid string mode raises ValueError.""" + with pytest.raises(ValueError, match="Unsupported STK application type: 'invalid'"): + launch_stk(mode="invalid") + + +def test_launch_stk_invalid_mode_type(): + """Test that invalid mode type raises ValueError.""" + with pytest.raises( + ValueError, + match="Mode must be an instance of STKApplicationType or a valid string value.", + ): + launch_stk(mode=123) + + +@patch("ansys.stk.extensions.launcher.STKEngine") +def test_launch_stk_no_configuration(mock_engine): + """Test launching STK without any configuration parameters.""" + mock_app = MagicMock() + mock_engine.start_application.return_value = mock_app + + result = launch_stk(mode="engine") + + mock_engine.start_application.assert_called_once_with() + assert result == mock_app + + +def test_enum_values(): + """Test that enum values are correct.""" + assert STKApplicationType.ENGINE.value == "engine" + assert STKApplicationType.DESKTOP.value == "desktop" + assert STKApplicationType.RUNTIME.value == "runtime" + + +def test_enum_string_conversion(): + """Test that string values can be converted to enum.""" + assert STKApplicationType("engine") == STKApplicationType.ENGINE + assert STKApplicationType("desktop") == STKApplicationType.DESKTOP + assert STKApplicationType("runtime") == STKApplicationType.RUNTIME + + +def test_enum_invalid_string(): + """Test that invalid string raises ValueError.""" + with pytest.raises(ValueError): + STKApplicationType("invalid_mode") + + +def test_enum_uniqueness(): + """Test that all enum values are unique.""" + values = [e.value for e in STKApplicationType] + assert len(values) == len(set(values)) + + +@patch("ansys.stk.extensions.launcher.STKDesktop") +def test_attach_to_application_desktop_with_string(mock_desktop): + """Test attaching to STK in DESKTOP mode using string mode.""" + mock_app = MagicMock() + mock_desktop.attach_to_application.return_value = mock_app + + result = attach_to_application(mode="desktop") + + mock_desktop.attach_to_application.assert_called_once_with() + assert result == mock_app + + +@patch("ansys.stk.extensions.launcher.STKDesktop") +def test_attach_to_application_desktop_with_enum(mock_desktop): + """Test attaching to STK in DESKTOP mode using enum mode.""" + mock_app = MagicMock() + mock_desktop.attach_to_application.return_value = mock_app + + result = attach_to_application(mode=STKApplicationType.DESKTOP) + + mock_desktop.attach_to_application.assert_called_once_with() + assert result == mock_app + + +def test_attach_to_application_runtime_with_string(): + """Test attaching to STK in RUNTIME mode using string mode.""" + launch_stk(mode="runtime", grpc_host="localhost", grpc_port=50051) + stk = attach_to_application(mode="runtime", grpc_host="localhost", grpc_port=50051) + stk.shutdown() + + +def test_attach_to_application_runtime_with_enum(): + """Test attaching to STK in RUNTIME mode using enum mode.""" + launch_stk(mode=STKApplicationType.RUNTIME, grpc_host="127.0.0.1", grpc_port=40704) + stk = attach_to_application(mode=STKApplicationType.RUNTIME, grpc_host="127.0.0.1", grpc_port=40704) + stk.shutdown() + + +def test_attach_to_application_engine_not_supported_string(): + """Test that ENGINE mode with string raises ValueError.""" + with pytest.raises( + NotImplementedError, + match="STKEngine mode does not support attaching to an existing instance." + ): + attach_to_application(mode="engine") + + +def test_attach_to_application_engine_not_supported_enum(): + """Test that ENGINE mode with enum raises ValueError.""" + with pytest.raises( + NotImplementedError, + match="STKEngine mode does not support attaching to an existing instance." + ): + attach_to_application(mode=STKApplicationType.ENGINE) + + +def test_attach_to_application_invalid_string_mode(): + """Test that invalid string mode raises ValueError.""" + with pytest.raises(ValueError, match="Unsupported STK application type: 'invalid'"): + attach_to_application(mode="invalid") + + +def test_attach_to_application_invalid_mode_type(): + """Test that invalid mode type raises ValueError.""" + with pytest.raises( + ValueError, + match="Mode must be an instance of STKApplicationType or a valid string value.", + ): + attach_to_application(mode=123) + + +@patch("ansys.stk.extensions.launcher.STKDesktop") +def test_attach_to_application_no_configuration(mock_desktop): + """Test attaching to STK without any configuration parameters.""" + mock_app = MagicMock() + mock_desktop.attach_to_application.return_value = mock_app + + result = attach_to_application(mode="desktop") + + mock_desktop.attach_to_application.assert_called_once_with() + assert result == mock_app diff --git a/tox.ini b/tox.ini index 7f304853e5..25497e9e60 100644 --- a/tox.ini +++ b/tox.ini @@ -91,7 +91,7 @@ usedevelop = true extras = snippets: grpc, extensions migration: migration - extensions: extensions + extensions: grpc, extensions dependency_groups = tests commands =