diff --git a/doc/changelog.d/454.added.md b/doc/changelog.d/454.added.md new file mode 100644 index 000000000..efd85aac0 --- /dev/null +++ b/doc/changelog.d/454.added.md @@ -0,0 +1 @@ +Feat/add local launcher \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 08feaa109..8f91548f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies=[ "grpcio>=1.50.0,<1.71", "grpcio-health-checking>=1.45.0,<1.68", "ansys-api-speos==0.14.2", + "ansys-tools-path>=0.3.1", "numpy>=1.20.3,<3", "comtypes>=1.4,<1.5", ] @@ -41,6 +42,7 @@ graphics = [ "ansys-tools-visualization-interface>=0.8.3", ] tests = [ + "psutil==6.1.1", "pytest==8.3.5", "pyvista>=0.40.0,<0.45", "ansys-tools-visualization-interface>=0.8.3", diff --git a/src/ansys/speos/core/generic/constants.py b/src/ansys/speos/core/generic/constants.py new file mode 100644 index 000000000..e922ebec6 --- /dev/null +++ b/src/ansys/speos/core/generic/constants.py @@ -0,0 +1,36 @@ +# Copyright (C) 2021 - 2025 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. + +"""Collection of all constants used in pySpeos.""" + +import os + +DEFAULT_HOST = "localhost" +"""Default host used by Speos RPC server and client """ +DEFAULT_PORT = "50098" +"""Default port used by Speos RPC server and client """ +DEFAULT_VERSION = "251" +"""Latest supported Speos version of the current PySpeos Package""" +MAX_MESSAGE_LENGTH = int(os.environ.get("SPEOS_MAX_MESSAGE_LENGTH", 256 * 1024**2)) +"""Maximum message length value accepted by the Speos RPC server, +By default, value stored in environment variable SPEOS_MAX_MESSAGE_LENGTH or 268 435 456. +""" diff --git a/src/ansys/speos/core/generic/general_methods.py b/src/ansys/speos/core/generic/general_methods.py index eb8676fef..ed4f68ee0 100644 --- a/src/ansys/speos/core/generic/general_methods.py +++ b/src/ansys/speos/core/generic/general_methods.py @@ -26,8 +26,14 @@ """ from functools import wraps +import os +from pathlib import Path +from typing import Optional, Union import warnings +from ansys.speos.core.generic.constants import DEFAULT_VERSION +from ansys.tools.path import get_available_ansys_installations + __GRAPHICS_AVAILABLE = None GRAPHICS_ERROR = ( "Preview unsupported without 'ansys-tools-visualization_interface' installed. " @@ -108,3 +114,67 @@ def wrapper(*args, **kwargs): return method(*args, **kwargs) return wrapper + + +def error_no_install(install_path: Union[Path, str], version: Union[int, str]): + """Raise error that installation was not found at a location. + + Parameters + ---------- + install_path : Union[Path, str] + Installation Path + version : Union[int, str] + Version + """ + install_loc_msg = "" + if install_path: + install_loc_msg = f"at {Path(install_path).parent}" + raise FileNotFoundError( + f"Ansys Speos RPC server installation not found{install_loc_msg}. " + f"Please define AWP_ROOT{version} environment variable" + ) + + +def retrieve_speos_install_dir( + speos_rpc_path: Optional[Union[Path, str]] = None, version: str = DEFAULT_VERSION +) -> Path: + """Retrieve Speos install location based on Path or Environment. + + Parameters + ---------- + speos_rpc_path : Optional[str, Path] + location of Speos rpc executable + version : Union[str, int] + The Speos server version to run, in the 3 digits format, such as "242". + If unspecified, the version will be chosen as + ``ansys.speos.core.kernel.client.LATEST_VERSION``. + + """ + if not speos_rpc_path: + speos_rpc_path = "" + if not speos_rpc_path or not Path(speos_rpc_path).exists(): + if not Path(speos_rpc_path).exists(): + warnings.warn( + "Provided executable location not found, looking for local installation", + UserWarning, + ) + versions = get_available_ansys_installations() + ansys_loc = versions.get(int(version), False) + if not ansys_loc: + ansys_loc = os.environ.get("AWP_ROOT{}".format(version), False) + if not ansys_loc: + error_no_install(speos_rpc_path, int(version)) + + speos_rpc_path = Path(ansys_loc) / "Optical Products" / "SPEOS_RPC" + elif Path(speos_rpc_path).is_file(): + if "SpeosRPC_Server" not in Path(speos_rpc_path).name: + error_no_install(speos_rpc_path, int(version)) + else: + speos_rpc_path = Path(speos_rpc_path).parent + if os.name == "nt": + speos_exec = speos_rpc_path / "SpeosRPC_Server.exe" + else: + speos_exec = speos_rpc_path / "SpeosRPC_Server.x" + if not speos_exec.is_file(): + error_no_install(speos_rpc_path, int(version)) + return speos_rpc_path diff --git a/src/ansys/speos/core/kernel/client.py b/src/ansys/speos/core/kernel/client.py index c196b8ef4..b9392890a 100644 --- a/src/ansys/speos/core/kernel/client.py +++ b/src/ansys/speos/core/kernel/client.py @@ -23,7 +23,9 @@ """Provides a wrapped abstraction of the gRPC proto API definition and stubs.""" import logging +import os from pathlib import Path +import subprocess import time from typing import TYPE_CHECKING, List, Optional, Union @@ -31,6 +33,8 @@ from grpc._channel import _InactiveRpcError from ansys.api.speos.part.v1 import body_pb2, face_pb2, part_pb2 +from ansys.speos.core.generic.constants import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSION +from ansys.speos.core.generic.general_methods import retrieve_speos_install_dir from ansys.speos.core.kernel.body import BodyLink, BodyStub from ansys.speos.core.kernel.face import FaceLink, FaceStub from ansys.speos.core.kernel.intensity_template import ( @@ -63,10 +67,6 @@ ) from ansys.speos.core.logger import LOG as LOGGER, PySpeosCustomAdapter -DEFAULT_HOST = "localhost" -DEFAULT_PORT = "50098" - - if TYPE_CHECKING: # pragma: no cover from ansys.platform.instancemanagement import Instance @@ -93,7 +93,7 @@ def wait_until_healthy(channel: grpc.Channel, timeout: float): try: grpc.channel_ready_future(channel).result(timeout=timeout) return True - except _InactiveRpcError: + except (_InactiveRpcError, grpc.FutureTimeoutError): continue else: target_str = channel._channel.target().decode() @@ -129,26 +129,44 @@ class SpeosClient: By default, ``INFO``. logging_file : Optional[str, Path] The file to output the log, if requested. By default, ``None``. + speos_install_path : Optional[str, Path] + location of Speos rpc executable """ def __init__( self, host: Optional[str] = DEFAULT_HOST, port: Union[str, int] = DEFAULT_PORT, + version: str = DEFAULT_VERSION, channel: Optional[grpc.Channel] = None, remote_instance: Optional["Instance"] = None, timeout: Optional[int] = 60, logging_level: Optional[int] = logging.INFO, logging_file: Optional[Union[Path, str]] = None, + speos_install_path: Optional[Union[Path, str]] = None, ): """Initialize the ``SpeosClient`` object.""" self._closed = False self._remote_instance = remote_instance + if speos_install_path: + speos_install_path = retrieve_speos_install_dir(speos_install_path, version) + if os.name == "nt": + self.__speos_exec = str(speos_install_path / "SpeosRPC_Server.exe") + else: + self.__speos_exec = str(speos_install_path / "SpeosRPC_Server.x") + else: + self.__speos_exec = None + if not version: + self._version = DEFAULT_VERSION + else: + self._version = version if channel: # Used for PyPIM when directly providing a channel self._channel = channel self._target = str(channel) else: + self._host = host + self._port = port self._target = f"{host}:{port}" self._channel = grpc.insecure_channel(self._target) # do not finish initialization until channel is healthy @@ -206,8 +224,7 @@ def target(self) -> str: def faces(self) -> FaceStub: """Get face database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._faceDB is None: self._faceDB = FaceStub(self._channel) @@ -215,8 +232,7 @@ def faces(self) -> FaceStub: def bodies(self) -> BodyStub: """Get body database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._bodyDB is None: self._bodyDB = BodyStub(self._channel) @@ -224,8 +240,7 @@ def bodies(self) -> BodyStub: def parts(self) -> PartStub: """Get part database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._partDB is None: self._partDB = PartStub(self._channel) @@ -233,8 +248,7 @@ def parts(self) -> PartStub: def sop_templates(self) -> SOPTemplateStub: """Get sop template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._sopTemplateDB is None: self._sopTemplateDB = SOPTemplateStub(self._channel) @@ -242,8 +256,7 @@ def sop_templates(self) -> SOPTemplateStub: def vop_templates(self) -> VOPTemplateStub: """Get vop template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._vopTemplateDB is None: self._vopTemplateDB = VOPTemplateStub(self._channel) @@ -251,8 +264,7 @@ def vop_templates(self) -> VOPTemplateStub: def spectrums(self) -> SpectrumStub: """Get spectrum database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._spectrumDB is None: self._spectrumDB = SpectrumStub(self._channel) @@ -260,8 +272,7 @@ def spectrums(self) -> SpectrumStub: def intensity_templates(self) -> IntensityTemplateStub: """Get intensity template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._intensityTemplateDB is None: self._intensityTemplateDB = IntensityTemplateStub(self._channel) @@ -269,8 +280,7 @@ def intensity_templates(self) -> IntensityTemplateStub: def source_templates(self) -> SourceTemplateStub: """Get source template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._sourceTemplateDB is None: self._sourceTemplateDB = SourceTemplateStub(self._channel) @@ -278,8 +288,7 @@ def source_templates(self) -> SourceTemplateStub: def sensor_templates(self) -> SensorTemplateStub: """Get sensor template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._sensorTemplateDB is None: self._sensorTemplateDB = SensorTemplateStub(self._channel) @@ -287,8 +296,7 @@ def sensor_templates(self) -> SensorTemplateStub: def simulation_templates(self) -> SimulationTemplateStub: """Get simulation template database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._simulationTemplateDB is None: self._simulationTemplateDB = SimulationTemplateStub(self._channel) @@ -296,8 +304,7 @@ def simulation_templates(self) -> SimulationTemplateStub: def scenes(self) -> SceneStub: """Get scene database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._sceneDB is None: self._sceneDB = SceneStub(self._channel) @@ -305,13 +312,17 @@ def scenes(self) -> SceneStub: def jobs(self) -> JobStub: """Get job database access.""" - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() # connect to database if self._jobDB is None: self._jobDB = JobStub(self._channel) return self._jobDB + def __closed_error(self): + """Check if closed.""" + if self._closed: + raise ConnectionAbortedError() + def __getitem__( self, key: str ) -> Union[ @@ -353,8 +364,7 @@ def __getitem__( None] Link object corresponding to the key - None if no objects corresponds to the key. """ - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() for sop in self.sop_templates().list(): if sop.key == key: return sop @@ -435,8 +445,7 @@ def get_items( List of Link objects corresponding to the keys - Empty if no objects corresponds to the keys. """ - if self._closed: - raise ConnectionAbortedError() + self.__closed_error() if item_type == SOPTemplateLink: return [x for x in self.sop_templates().list() if x.key in keys] @@ -483,14 +492,24 @@ def __repr__(self) -> str: def close(self): """Close the channel. + Returns + ------- + bool + Information if the server instance was terminated. + Notes ----- If an instance of the Speos Service was started using PyPIM, this instance will be deleted. """ + wait_time = 0 if self._remote_instance: self._remote_instance.delete() - self._closed = True + elif self._host in ["localhost", "0.0.0.0", "127.0.0.1"] and self.__speos_exec: + self.__close_local_speos_rpc_server() + while self.healthy and wait_time < 15: + time.sleep(1) + wait_time += 1 # takes some seconds to close rpc server self._channel.close() self._faceDB = None self._bodyDB = None @@ -504,3 +523,13 @@ def close(self): self._simulationTemplateDB = None self._sceneDB = None self._jobDB = None + if wait_time >= 15: + self._closed = not self.healthy + return self._closed + else: + self._closed = True + return self._closed + + def __close_local_speos_rpc_server(self): + command = [self.__speos_exec, "-s{}".format(self._port)] + subprocess.run(command, check=True) diff --git a/src/ansys/speos/core/launcher.py b/src/ansys/speos/core/launcher.py index e407a026a..0655307d5 100644 --- a/src/ansys/speos/core/launcher.py +++ b/src/ansys/speos/core/launcher.py @@ -23,12 +23,16 @@ """Module to start Speos RPC Server.""" import os +from pathlib import Path +import subprocess +import tempfile +from typing import Optional, Union from ansys.speos.core import LOG as LOGGER +from ansys.speos.core.generic.constants import DEFAULT_PORT, DEFAULT_VERSION, MAX_MESSAGE_LENGTH +from ansys.speos.core.generic.general_methods import retrieve_speos_install_dir from ansys.speos.core.speos import Speos -MAX_MESSAGE_LENGTH = int(os.environ.get("SPEOS_MAX_MESSAGE_LENGTH", 256 * 1024**2)) - try: import ansys.platform.instancemanagement as pypim @@ -93,3 +97,78 @@ def launch_remote_speos( instance.wait_for_ready() channel = instance.build_grpc_channel() return Speos(channel=channel, remote_instance=instance) + + +def launch_local_speos_rpc_server( + version: str = DEFAULT_VERSION, + port: Union[str, int] = DEFAULT_PORT, + message_size: int = MAX_MESSAGE_LENGTH, + logfile_loc: str = None, + log_level: int = 20, + speos_rpc_path: Optional[Union[Path, str]] = None, +) -> Speos: + """Launch Speos RPC server locally. + + .. warning:: + Do not execute this function with untrusted input parameters. + + Parameters + ---------- + version : str + The Speos server version to run, in the 3 digits format, such as "242". + If unspecified, the version will be chosen as + ``ansys.speos.core.kernel.client.LATEST_VERSION``. + port : Union[str, int], optional + Port number where the server is running. + By default, ``ansys.speos.core.kernel.client.DEFAULT_PORT``. + message_size : int + Maximum message length value accepted by the Speos RPC server, + By default, value stored in environment variable SPEOS_MAX_MESSAGE_LENGTH or 268 435 456. + logfile_loc : str + location for the logfile to be created in. + log_level : int + The logging level to be applied to the server, integer values can be taken from logging + module. + By default, ``logging.WARNING`` = 20. + speos_rpc_path : Optional[str, Path] + location of Speos rpc executable + + Returns + ------- + ansys.speos.core.speos.Speos + An instance of the Speos Service. + """ + speos_rpc_path = retrieve_speos_install_dir(speos_rpc_path, version) + if os.name == "nt": + speos_exec = speos_rpc_path / "SpeosRPC_Server.exe" + else: + speos_exec = speos_rpc_path / "SpeosRPC_Server.x" + if not logfile_loc: + logfile_loc = Path(tempfile.gettempdir()) / ".ansys" + logfile = logfile_loc / "speos_rpc.log" + else: + logfile = Path(logfile_loc) + if logfile.is_file(): + logfile_loc = logfile.parent + else: + logfile_loc = Path(logfile_loc) + logfile = logfile_loc / "speos_rpc.log" + if not logfile_loc.exists(): + logfile_loc.mkdir() + command = [ + str(speos_exec), + "-p{}".format(port), + "-m{}".format(message_size), + "-l{}".format(str(logfile)), + ] + out, stdout_file = tempfile.mkstemp(suffix="speos_out.txt", dir=logfile_loc) + err, stderr_file = tempfile.mkstemp(suffix="speos_err.txt", dir=logfile_loc) + + subprocess.Popen(command, stdout=out, stderr=err) + return Speos( + host="localhost", + port=port, + logging_level=log_level, + logging_file=logfile, + speos_install_path=speos_rpc_path, + ) diff --git a/src/ansys/speos/core/logger.py b/src/ansys/speos/core/logger.py index f6d6b7e5e..b4957d6bf 100644 --- a/src/ansys/speos/core/logger.py +++ b/src/ansys/speos/core/logger.py @@ -135,7 +135,7 @@ # For convenience DEBUG = logging.DEBUG INFO = logging.INFO -WARN = logging.WARN +WARN = logging.WARNING ERROR = logging.ERROR CRITICAL = logging.CRITICAL diff --git a/src/ansys/speos/core/speos.py b/src/ansys/speos/core/speos.py index b93649869..5252f87cd 100644 --- a/src/ansys/speos/core/speos.py +++ b/src/ansys/speos/core/speos.py @@ -28,12 +28,9 @@ from grpc import Channel +from ansys.speos.core.generic.constants import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_VERSION from ansys.speos.core.kernel.client import SpeosClient -DEFAULT_HOST = "localhost" -DEFAULT_PORT = "50098" - - if TYPE_CHECKING: # pragma: no cover from ansys.platform.instancemanagement import Instance @@ -45,10 +42,14 @@ class Speos: ---------- host : str, optional Host where the server is running. - By default, ``DEFAULT_HOST``. + By default, ``ansys.speos.core.kernel.client.DEFAULT_HOST``. port : Union[str, int], optional Port number where the server is running. - By default, ``DEFAULT_PORT``. + By default, ``ansys.speos.core.kernel.client.DEFAULT_PORT``. + version : str + The Speos server version to run, in the 3 digits format, such as "242". + If unspecified, the version will be chosen as + ``ansys.speos.core.kernel.client.LATEST_VERSION``. channel : ~grpc.Channel, optional gRPC channel for server communication. By default, ``None``. @@ -70,23 +71,42 @@ def __init__( self, host: str = DEFAULT_HOST, port: Union[str, int] = DEFAULT_PORT, + version: str = DEFAULT_VERSION, channel: Optional[Channel] = None, remote_instance: Optional["Instance"] = None, timeout: Optional[int] = 60, logging_level: Optional[int] = logging.INFO, logging_file: Optional[Union[Path, str]] = None, + speos_install_path: Optional[Union[Path, str]] = None, ): self._client = SpeosClient( host=host, port=port, + version=version, channel=channel, remote_instance=remote_instance, timeout=timeout, logging_level=logging_level, logging_file=logging_file, + speos_install_path=speos_install_path, ) @property def client(self) -> SpeosClient: """The ``Speos`` instance client.""" return self._client + + def close(self) -> bool: + """Close the channel and deletes all Speos objects from memory. + + Returns + ------- + bool + Information if the server instance was terminated. + + Notes + ----- + If an instance of the Speos Service was started using + PyPIM, this instance will be deleted. + """ + return self.client.close() diff --git a/tests/conftest.py b/tests/conftest.py index b6f94501b..4988af1ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -47,6 +47,7 @@ pass IMAGE_RESULTS_DIR = Path(Path(__file__).parent, "image_results") +IS_WINDOWS = os.name == "nt" @pytest.fixture(scope="session") diff --git a/tests/core/test_launcher.py b/tests/core/test_launcher.py new file mode 100644 index 000000000..0b9c96de0 --- /dev/null +++ b/tests/core/test_launcher.py @@ -0,0 +1,94 @@ +# Copyright (C) 2021 - 2025 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. + +"""Test launcher.""" + +import os +from pathlib import Path +import subprocess +import tempfile +from unittest.mock import patch + +import psutil +import pytest + +from ansys.speos.core.generic.constants import DEFAULT_VERSION +from ansys.speos.core.launcher import launch_local_speos_rpc_server +from tests.conftest import IS_WINDOWS, config + +IS_DOCKER = config.get("SpeosServerOnDocker") + + +@pytest.mark.skipif(IS_DOCKER, reason="launcher only works without Docker image") +def test_local_session(*args): + """Test local session launch and close.""" + port = config.get("SpeosServerPort") + 1 + if IS_WINDOWS: + speos_loc = None + name = "SpeosRPC_Server.exe" + else: + speos_loc = None + name = "SpeosRPC_Server.x" + p_list = [p.name() for p in psutil.process_iter()] + nb_process = p_list.count(name) + test_speos = launch_local_speos_rpc_server(port=port, speos_rpc_path=speos_loc) + p_list = [p.name() for p in psutil.process_iter()] + running = p_list.count(name) > nb_process + assert running is test_speos.client.healthy + closed = test_speos.close() + p_list = [p.name() for p in psutil.process_iter()] + running = p_list.count(name) > nb_process + assert running is not closed + + +@patch.object(subprocess, "Popen") +@patch.object(subprocess, "run") +def test_coverage_launcher_speosdocker(*args): + """Test local session launch on remote server to improve coverage.""" + port = config.get("SpeosServerPort") + tmp_file = tempfile.gettempdir() + if IS_WINDOWS: + name = "SpeosRPC_Server.exe" + else: + name = "SpeosRPC_Server.x" + speos_loc = Path(tmp_file) / "Optical Products" / "SPEOS_RPC" / name + speos_loc.parent.parent.mkdir(exist_ok=True) + speos_loc.parent.mkdir(exist_ok=True) + if not speos_loc.exists(): + f = speos_loc.open("w") + f.write("speos_test_file") + f.close() + os.environ["AWP_ROOT{}".format(DEFAULT_VERSION)] = tmp_file + test_speos = launch_local_speos_rpc_server(port=port) + assert True is test_speos.client.healthy + assert True is test_speos.close() + assert False is test_speos.client.healthy + test_speos = launch_local_speos_rpc_server( + port=port, speos_rpc_path=speos_loc, logfile_loc=tmp_file + ) + assert True is test_speos.client.healthy + test_speos.client._closed = True + assert True is test_speos.close() + assert False is test_speos.client.healthy + speos_loc.unlink() + speos_loc.parent.rmdir() + speos_loc.parent.parent.rmdir()