diff --git a/src/ansys/fluent/core/exceptions.py b/src/ansys/fluent/core/exceptions.py new file mode 100644 index 00000000000..ce3f0053dec --- /dev/null +++ b/src/ansys/fluent/core/exceptions.py @@ -0,0 +1,35 @@ +"""Custom common higher level exceptions.""" + +from typing import Any, Optional + +from ansys.fluent.core.solver.error_message import allowed_name_error_message + + +class DisallowedValuesError(ValueError): + """Provides the error when an argument value is not in allowed values.""" + + def __init__( + self, + context: Optional[Any] = None, + name: Optional[Any] = None, + allowed_values: Optional[Any] = None, + ): + super().__init__( + allowed_name_error_message( + context=context, trial_name=name, allowed_values=allowed_values + ) + ) + + +class InvalidArgument(ValueError): + """Provides the error when an argument value is inappropriate.""" + + pass + + +class SurfaceSpecificationError(ValueError): + """Provides the error when both ``surface_ids`` and ``surface_names`` are + provided.""" + + def __init__(self): + super().__init__("Provide either 'surface_ids' or 'surface_names'.") diff --git a/src/ansys/fluent/core/file_session.py b/src/ansys/fluent/core/file_session.py index 551cc39e9c9..7c2da7836fc 100644 --- a/src/ansys/fluent/core/file_session.py +++ b/src/ansys/fluent/core/file_session.py @@ -3,6 +3,7 @@ import numpy as np from ansys.api.fluent.v0.field_data_pb2 import DataLocation +from ansys.fluent.core.exceptions import SurfaceSpecificationError from ansys.fluent.core.filereader.case_file import CaseFile from ansys.fluent.core.filereader.data_file import DataFile from ansys.fluent.core.services.field_data import ( @@ -14,6 +15,20 @@ ) +class InvalidMultiPhaseFieldName(ValueError): + """Provides the error when multi-phase field name is inappropriate.""" + + def __init__(self): + super().__init__("Multi-phase field name should start with 'phase-'.") + + +class InvalidFieldName(ValueError): + """Provides the error when a field name is inappropriate.""" + + def __init__(self): + super().__init__("The only allowed field is 'velocity'.") + + class Transaction: """Populates field data on surfaces.""" @@ -108,15 +123,15 @@ def add_scalar_fields_request( Raises ------ - RuntimeError - If surface names or surface ids are not provided. - RuntimeError + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. + InvalidMultiPhaseFieldName If field name does not have prefix ``phase-`` for multi-phase cases. """ if surface_ids is None: surface_ids = [] if surface_ids and surface_names: - raise RuntimeError("Please provide either surface names or surface ids.") + raise SurfaceSpecificationError() if surface_names: for surface_name in surface_names: @@ -126,9 +141,7 @@ def add_scalar_fields_request( if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() self._scalar_field_transactions.append( Transaction._ScalarFieldTransaction( field_name, surface_ids, field_name.split(":")[0] @@ -162,15 +175,15 @@ def add_vector_fields_request( Raises ------ - RuntimeError - If surface names or surface ids are not provided. - RuntimeError + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. + InvalidMultiPhaseFieldName If field name does not have prefix ``phase-`` for multi-phase cases. """ if surface_ids is None: surface_ids = [] if surface_ids and surface_names: - raise RuntimeError("Please provide either surface names or surface ids.") + raise SurfaceSpecificationError() if surface_names: for surface_name in surface_names: @@ -180,9 +193,7 @@ def add_vector_fields_request( if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() self._vector_field_transactions.append( Transaction._VectorFieldTransaction( field_name, surface_ids, field_name.split(":")[0] @@ -227,7 +238,7 @@ def get_fields(self): Raises ------ - RuntimeError + InvalidFieldName If any field other than ``"velocity"`` is provided. """ mesh = self._file_session._case_file.get_mesh() @@ -258,7 +269,7 @@ def get_fields(self): for transaction in self._vector_field_transactions: if "velocity" not in transaction.field_name: - raise RuntimeError("Only 'velocity' is allowed field.") + raise InvalidFieldName() if vector_field_tag not in field_data: field_data[vector_field_tag] = {} field_data_surface = field_data[vector_field_tag] @@ -323,11 +334,11 @@ def get_surface_data( Raises ------ - RuntimeError - If surface names or surface ids are not provided. + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. """ if surface_ids and surface_name: - raise RuntimeError("Please provide either surface name or surface ids.") + raise SurfaceSpecificationError() if data_type == SurfaceDataType.Vertices: if surface_name: @@ -407,15 +418,13 @@ def get_scalar_field_data( Raises ------ - RuntimeError - If surface names or surface ids are not provided. - RuntimeError - If field name does not have prefix ``phase-`` for multi-phase cases. - RuntimeError + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. + InvalidMultiPhaseFieldName If field name does not have prefix ``phase-`` for multi-phase cases. """ if surface_ids and surface_name: - raise RuntimeError("Please provide either surface name or surface ids.") + raise SurfaceSpecificationError() if surface_name: surface_ids = self._field_info.get_surfaces_info()[surface_name][ @@ -423,9 +432,7 @@ def get_scalar_field_data( ] if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() return ScalarFieldData( surface_ids[0], self._file_session._data_file.get_face_scalar_field_data( @@ -444,9 +451,7 @@ def get_scalar_field_data( else: if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() return { surface_id: ScalarFieldData( surface_id, @@ -495,23 +500,21 @@ def get_vector_field_data( Raises ------ - RuntimeError - If surface names or surface ids are not provided. - RuntimeError + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. + InvalidFieldName If any field other than ``"velocity"`` is provided. - RuntimeError - If field name does not have prefix ``phase-`` for multi-phase cases. - RuntimeError + InvalidMultiPhaseFieldName If field name does not have prefix ``phase-`` for multi-phase cases. """ if surface_ids and surface_name: - raise RuntimeError("Please provide either surface name or surface ids.") + raise SurfaceSpecificationError() if ( field_name.lower() != "velocity" and field_name.split(":")[1].lower() != "velocity" ): - raise RuntimeError("Only 'velocity' is allowed field.") + raise InvalidFieldName() if surface_name: surface_ids = self._field_info.get_surfaces_info()[surface_name][ @@ -519,9 +522,7 @@ def get_vector_field_data( ] if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() vector_data = self._file_session._data_file.get_face_vector_field_data( field_name.split(":")[0], surface_ids[0] ) @@ -534,9 +535,7 @@ def get_vector_field_data( else: if len(self._file_session._data_file.get_phases()) > 1: if not field_name.startswith("phase-"): - raise RuntimeError( - "For multi-phase cases field name should have a prefix of phase name." - ) + raise InvalidMultiPhaseFieldName() return { surface_id: VectorFieldData( surface_id, diff --git a/src/ansys/fluent/core/fluent_connection.py b/src/ansys/fluent/core/fluent_connection.py index be40c0ea3c6..e3733653443 100644 --- a/src/ansys/fluent/core/fluent_connection.py +++ b/src/ansys/fluent/core/fluent_connection.py @@ -24,6 +24,30 @@ logger = logging.getLogger("pyfluent.general") +class PortNotProvided(ValueError): + """Provides the error when port is not provided.""" + + def __init__(self): + super().__init__( + "Provide the 'port' to connect to an existing Fluent instance." + ) + + +class UnsupportedRemoteFluentInstance(ValueError): + """Provides the error when 'wait_process_finished' does not support remote Fluent + session.""" + + def __init__(self): + super().__init__("Remote Fluent instance is unsupported.") + + +class WaitTypeError(TypeError): + """Provides the error when invalid ``wait`` type is provided.""" + + def __init__(self): + super().__init__("Invalid 'wait' type.") + + def _get_max_c_int_limit() -> int: """Get the maximum limit of a C int. @@ -236,6 +260,11 @@ def __init__( inside_container: bool, optional Whether the Fluent session that is being connected to is running inside a docker container. + + Raises + ------ + PortNotProvided + If port is not provided. """ self.error_state = ErrorState() self._data_valid = False @@ -250,9 +279,7 @@ def __init__( port = os.getenv("PYFLUENT_FLUENT_PORT") self._channel_str = f"{ip}:{port}" if not port: - raise ValueError( - "The port to connect to Fluent session is not provided." - ) + raise PortNotProvided() # Same maximum message length is used in the server max_message_length = _get_max_c_int_limit() self._channel = grpc.insecure_channel( @@ -495,15 +522,13 @@ def wait_process_finished(self, wait: Union[float, int, bool] = 60): Raises ------ - ValueError + UnsupportedRemoteFluentInstance If current Fluent instance is running remotely. - TypeError + WaitTypeError If ``wait`` is specified improperly. """ if self._remote_instance: - raise ValueError( - "Fluent remote instance not supported by FluentConnection.wait_process_finished()." - ) + raise UnsupportedRemoteFluentInstance() if isinstance(wait, bool): if wait: wait = 60 @@ -513,7 +538,7 @@ def wait_process_finished(self, wait: Union[float, int, bool] = 60): if isinstance(wait, (float, int)): logger.info(f"Waiting {wait} seconds for Fluent processes to finish...") else: - raise TypeError("Invalid 'limit' type.") + raise WaitTypeError() if self.connection_properties.inside_container: _response = timeout_loop( get_container, diff --git a/src/ansys/fluent/core/launcher/fluent_container.py b/src/ansys/fluent/core/launcher/fluent_container.py index 2008c0c27c1..ab28d2b19fd 100644 --- a/src/ansys/fluent/core/launcher/fluent_container.py +++ b/src/ansys/fluent/core/launcher/fluent_container.py @@ -64,6 +64,33 @@ DEFAULT_CONTAINER_MOUNT_PATH = "/mnt/pyfluent" +class FluentImageNameTagNotSpecified(ValueError): + """Provides the error when Fluent image name or image tag is not specified.""" + + def __init__(self): + super().__init__( + "Specify either 'fluent_image' or 'image_tag' and 'image_name'." + ) + + +class ServerInfoFileError(ValueError): + """Provides the error when server info file is not given properly.""" + + def __init__(self): + super().__init__( + "Specify server info file either using 'container_server_info_file' argument or in the 'container_dict'." + ) + + +class LicenseServerNotSpecified(KeyError): + """Provides the error when license server is not specified.""" + + def __init__(self): + super().__init__( + "Specify licence server either using 'ANSYSLMD_LICENSE_FILE' environment variable or in the 'container_dict'." + ) + + def configure_container_dict( args: List[str], host_mount_path: Optional[Union[str, Path]] = None, @@ -120,11 +147,11 @@ def configure_container_dict( Raises ------ - KeyError + LicenseServerNotSpecified If license server is not specified through an environment variable or in ``container_dict``. - ValueError + ServerInfoFileError If server info file is specified through both a command-line argument inside ``container_dict`` and the ``container_server_info_file`` parameter. - ValueError + FluentImageNameTagNotSpecified If ``fluent_image`` or ``image_tag`` and ``image_name`` are not specified. Notes @@ -208,10 +235,7 @@ def configure_container_dict( license_server = os.getenv("ANSYSLMD_LICENSE_FILE") if not license_server: - raise KeyError( - "License server needs to be specified through an environment variable, " - "or in the `container_dict`." - ) + raise LicenseServerNotSpecified() container_dict.update( environment={ "ANSYSLMD_LICENSE_FILE": license_server, @@ -234,10 +258,7 @@ def configure_container_dict( for v in container_dict["command"]: if v.startswith("-sifile="): if container_server_info_file: - raise ValueError( - "Specified a server info file command argument as well as " - "a container_server_info_file, pick one." - ) + raise ServerInfoFileError() container_server_info_file = PurePosixPath( v.replace("-sifile=", "") ).name @@ -269,9 +290,7 @@ def configure_container_dict( elif image_tag and image_name: fluent_image = f"{image_name}:{image_tag}" else: - raise ValueError( - "Missing 'fluent_image', or 'image_tag' and 'image_name', specification for Docker container launch." - ) + raise FluentImageNameTagNotSpecified() container_dict["fluent_image"] = fluent_image @@ -320,7 +339,7 @@ def start_fluent_container( Raises ------ - RuntimeError + TimeoutError If Fluent container launch reaches timeout. Notes @@ -377,8 +396,8 @@ def start_fluent_container( ) if not success: - raise RuntimeError( - "Fluent container launch timeout, will have to stop container manually." + raise TimeoutError( + "Fluent container launch has timed out, stop container manually." ) else: _, _, password = _parse_server_info_file(str(host_server_info_file)) diff --git a/src/ansys/fluent/core/launcher/launcher.py b/src/ansys/fluent/core/launcher/launcher.py index 8a5d3098a6e..649cc2769c4 100644 --- a/src/ansys/fluent/core/launcher/launcher.py +++ b/src/ansys/fluent/core/launcher/launcher.py @@ -15,7 +15,8 @@ import time from typing import Any, Dict, List, Optional, Union -from ansys.fluent.core.fluent_connection import FluentConnection +from ansys.fluent.core.exceptions import DisallowedValuesError, InvalidArgument +from ansys.fluent.core.fluent_connection import FluentConnection, PortNotProvided from ansys.fluent.core.launcher.fluent_container import ( configure_container_dict, start_fluent_container, @@ -39,6 +40,40 @@ logger = logging.getLogger("pyfluent.launcher") +class AnsysVersionNotFound(RuntimeError): + """Provides the error when Ansys version is not found.""" + + def __init__(self): + super().__init__("Verify the value of the 'AWP_ROOT' environment variable.") + + +class InvalidPassword(ValueError): + """Provides the error when password is invalid.""" + + def __init__(self): + super().__init__("Provide correct 'password'.") + + +class IpPortNotProvided(ValueError): + """Provides the error when ip and port are not specified.""" + + def __init__(self): + super().__init__("Provide either 'ip' and 'port' or 'server_info_file_name'.") + + +class UnexpectedKeywordArgument(TypeError): + """Provides the error when a valid keyword argument is not specified.""" + + pass + + +class DockerContainerLaunchNotSupported(SystemError): + """Provides the error when docker container launch is not supported.""" + + def __init__(self): + super().__init__("Python Docker SDK is unsupported on this system.") + + def _is_windows(): """Check if the current operating system is windows.""" return platform.system() == "Windows" @@ -104,14 +139,14 @@ def get_ansys_version() -> str: Raises ------ - RuntimeError + AnsysVersionNotFound If an Ansys version cannot be found. """ for v in FluentVersion: if "AWP_ROOT" + "".join(str(v).split("."))[:-1] in os.environ: return str(v) - raise RuntimeError("An Ansys version cannot be found.") + raise AnsysVersionNotFound() def get_fluent_exe_path(**launch_argvals) -> Path: @@ -177,15 +212,15 @@ def get_mode(mode: str) -> "FluentMode": Raises ------ - RuntimeError + DisallowedValuesError If an unknown mode is passed. """ for m in FluentMode: if mode == m.value[0]: return m else: - raise RuntimeError( - f"The passed mode '{mode}' matches none of the allowed modes." + raise DisallowedValuesError( + "mode", mode, ["meshing", "pure-meshing", "solver", "solver-icing"] ) @@ -366,7 +401,7 @@ def _raise_exception_g_gu_in_windows_os(additional_arguments: str) -> None: if _is_windows() and ( ("-g" in additional_arg_list) or ("-gu" in additional_arg_list) ): - raise ValueError("'-g' and '-gu' is not supported on windows platform.") + raise InvalidArgument("Unsupported '-g' and '-gu' on windows platform.") def _update_launch_string_wrt_gui_options( @@ -394,7 +429,7 @@ def _await_fluent_launch( logger.info("Fluent has been successfully launched.") break if start_timeout == 0: - raise RuntimeError("The launch process has been timed out.") + raise TimeoutError("The launch process has timed out.") time.sleep(1) start_timeout -= 1 logger.info(f"Waiting for Fluent to launch...{start_timeout} seconds remaining") @@ -407,18 +442,20 @@ def _get_server_info( password: Optional[str] = None, ): """Get server connection information of an already running session.""" - if ip and port: + if not (ip and port) and not server_info_file_name: + raise IpPortNotProvided() + if (ip or port) and server_info_file_name: logger.warning( - "Could not parse server-info file because ip and port were provided explicitly." + "The ip and port will be extracted from the server-info file and their explicitly specified values will be ignored." ) - elif server_info_file_name: - ip, port, password = _parse_server_info_file(server_info_file_name) - elif os.getenv("PYFLUENT_FLUENT_IP") and os.getenv("PYFLUENT_FLUENT_PORT"): - ip = port = None else: - raise RuntimeError( - "Please provide either ip and port data or server-info file." - ) + if server_info_file_name: + ip, port, password = _parse_server_info_file(server_info_file_name) + ip = ip or os.getenv("PYFLUENT_FLUENT_IP", "127.0.0.1") + port = port or os.getenv("PYFLUENT_FLUENT_PORT") + + if not port: + raise PortNotProvided() return ip, port, password @@ -438,7 +475,7 @@ def _get_running_session_mode( else "meshing" ) except Exception as ex: - raise RuntimeError("Fluent session password mismatch") from ex + raise InvalidPassword() from ex return session_mode.value[1] @@ -626,6 +663,13 @@ def launch_fluent( :class:`~ansys.fluent.core.session_solver_icing.SolverIcing`, dict] Session object or configuration dictionary if ``dry_run = True``. + Raises + ------ + UnexpectedKeywordArgument + If an unexpected keyword argument is provided. + DockerContainerLaunchNotSupported + If a Fluent Docker container launch is not supported. + Notes ----- Job scheduler environments such as SLURM, LSF, PBS, etc. allocates resources / compute nodes. @@ -634,12 +678,11 @@ def launch_fluent( """ if kwargs: if "meshing_mode" in kwargs: - raise RuntimeError( - "'meshing_mode' argument is no longer used." - " Please use launch_fluent(mode='meshing') to launch in meshing mode." + raise UnexpectedKeywordArgument( + "Use 'launch_fluent(mode='meshing')' to launch Fluent in meshing mode." ) else: - raise TypeError( + raise UnexpectedKeywordArgument( f"launch_fluent() got an unexpected keyword argument {next(iter(kwargs))}" ) del kwargs @@ -653,10 +696,7 @@ def launch_fluent( if check_docker_support(): fluent_launch_mode = LaunchMode.CONTAINER else: - raise SystemError( - "Docker is not working correctly in this system, " - "yet a Fluent Docker container launch was specified." - ) + raise DockerContainerLaunchNotSupported() else: fluent_launch_mode = LaunchMode.STANDALONE @@ -731,13 +771,15 @@ def launch_fluent( kwargs.update(cwd=cwd) if journal_file_names: if not isinstance(journal_file_names, (str, list)): - raise TypeError("Journal name should be a list of strings.") + raise InvalidArgument( + "'journal_file_names' should be a list of strings." + ) if isinstance(journal_file_names, str): journal_file_names = [journal_file_names] if topy: if not journal_file_names: - raise RuntimeError( - "Please provide the journal files to be converted as 'journal_file_names' argument." + raise InvalidArgument( + "Use 'journal_file_names' to specify and convert journal files." ) launch_string += scm_to_py(topy, journal_file_names) @@ -756,7 +798,7 @@ def launch_fluent( _await_fluent_launch( server_info_file_name, start_timeout, sifile_last_mtime ) - except RuntimeError as ex: + except TimeoutError as ex: if _is_windows(): logger.warning(f"Exception caught - {type(ex).__name__}: {ex}") launch_cmd = launch_string.replace('"', "", 2) diff --git a/src/ansys/fluent/core/launcher/watchdog.py b/src/ansys/fluent/core/launcher/watchdog.py index e3369b6bbac..3f450b92568 100644 --- a/src/ansys/fluent/core/launcher/watchdog.py +++ b/src/ansys/fluent/core/launcher/watchdog.py @@ -22,6 +22,12 @@ WATCHDOG_INIT_FILE = "watchdog_{}_init" +class UnsuccessfulWatchdogLaunch(RuntimeError): + """Provides the error when watchdog launch is unsuccessful.""" + + pass + + def launch( main_pid: int, sv_port: int, sv_password: str, sv_ip: Optional[str] = None ) -> None: @@ -40,7 +46,7 @@ def launch( Raises ------ - RuntimeError + UnsuccessfulWatchdogLaunch If Watchdog fails to launch. """ watchdog_id = "".join( @@ -168,10 +174,12 @@ def launch( watchdog_err.unlink() logger.error(err_content) if os.getenv("PYFLUENT_WATCHDOG_EXCEPTION_ON_ERROR"): - raise RuntimeError(err_content) + raise UnsuccessfulWatchdogLaunch(err_content) logger.warning( "PyFluent Watchdog did not initialize correctly, proceeding without it..." ) if os.getenv("PYFLUENT_WATCHDOG_EXCEPTION_ON_ERROR"): - raise RuntimeError("PyFluent Watchdog did not initialize correctly.") + raise UnsuccessfulWatchdogLaunch( + "PyFluent Watchdog did not initialize correctly." + ) diff --git a/src/ansys/fluent/core/meta.py b/src/ansys/fluent/core/meta.py index 5f9bf86a0b8..dd56d0723f0 100644 --- a/src/ansys/fluent/core/meta.py +++ b/src/ansys/fluent/core/meta.py @@ -4,6 +4,8 @@ import inspect from pprint import pformat +from ansys.fluent.core.exceptions import DisallowedValuesError, InvalidArgument + # pylint: disable=unused-private-member # pylint: disable=bad-mcs-classmethod-argument @@ -35,10 +37,7 @@ def __init__(self, function): def __set_name__(self, obj, name): if name not in self.VALID_NAMES: - raise ValueError( - f"Attribute {name} is not allowed." - f"Expected values are {self.VALID_NAMES}" - ) + raise DisallowedValuesError("attribute", name, self.VALID_NAMES) if not hasattr(obj, "attributes"): obj.attributes = set() obj.attributes.add(name) @@ -78,13 +77,13 @@ def _execute(_self, *args, **kwargs): if not all( elem in allowed_values for elem in arg_value ): - raise RuntimeError( - f"All values of {arg} value {arg_value} is not within allowed values." + raise DisallowedValuesError( + arg, arg_value, allowed_values ) else: if arg_value not in allowed_values: - raise RuntimeError( - f"{arg} value {arg_value} is not within allowed values." + raise DisallowedValuesError( + arg, arg_value, allowed_values ) elif attr == "range": @@ -95,8 +94,8 @@ def _execute(_self, *args, **kwargs): minimum, maximum = attr_value(_self.obj) if arg_value < minimum or arg_value > maximum: - raise RuntimeError( - f"{arg} value {arg_value} is not within range." + raise DisallowedValuesError( + arg, arg_value, allowed_values ) return method(_self.obj, *args, **kwargs) @@ -137,7 +136,7 @@ def wrapper(attribute): {attribute.__name__: attribute} ) else: - raise RuntimeError(f"{argument_name} not a valid argument.") + raise InvalidArgument(f"{argument_name} not a valid argument.") return attribute return wrapper @@ -232,26 +231,19 @@ def wrapper(self, value): if self.range and ( value < self.range[0] or value > self.range[1] ): - raise ValueError( - f"Value {value}, is not within valid range" - f" {self.range}." - ) + raise DisallowedValuesError("value", value, self.range) elif attr == "allowed_values": if isinstance(value, list): if not all( v is None or v in self.allowed_values for v in value ): - raise ValueError( - f"Not all values in {value}, are in the " - "list of allowed values " - f"{self.allowed_values}." + raise DisallowedValuesError( + "value", value, self.allowed_values ) elif value is not None and value not in self.allowed_values: - raise ValueError( - f"Value {value}, is not in the list of " - f"allowed values {self.allowed_values}." + raise DisallowedValuesError( + "value", value, self.allowed_values ) - return value return wrapper diff --git a/src/ansys/fluent/core/post_objects/post_helper.py b/src/ansys/fluent/core/post_objects/post_helper.py index 162fd27ab99..9650a23f717 100644 --- a/src/ansys/fluent/core/post_objects/post_helper.py +++ b/src/ansys/fluent/core/post_objects/post_helper.py @@ -1,6 +1,20 @@ import re +class IncompleteISOSurfaceDefinition(RuntimeError): + """Provides the error when iso-surface definition is incomplete.""" + + def __init__(self): + super().__init__("Iso surface definition is incomplete.") + + +class SurfaceCreationError(RuntimeError): + """Provides the error when surface creation is unsuccessful.""" + + def __init__(self): + super().__init__("Surface creation is unsuccessful.") + + class PostAPIHelper: """Class providing helper API for post objects.""" @@ -30,9 +44,9 @@ def create_surface_on_server(self): Raises ------ - RuntimeError + IncompleteISOSurfaceDefinition If iso-surface definition is incomplete. - RuntimeError + SurfaceCreationError If server fails to create surface. """ if self.obj.definition.type() == "iso-surface": @@ -40,7 +54,7 @@ def create_surface_on_server(self): field = iso_surface.field() iso_value = iso_surface.iso_value() if not field: - raise RuntimeError("Iso surface definition is incomplete.") + raise IncompleteISOSurfaceDefinition() self._delete_if_exist_on_server() phases = self.obj._api_helper._get_phases() unit_quantity = self.obj._api_helper._field_unit_quantity(field) @@ -91,7 +105,7 @@ def create_surface_on_server(self): field_info = self.obj._api_helper.field_info() surfaces_list = list(field_info.get_surfaces_info().keys()) if self._surface_name_on_server not in surfaces_list: - raise RuntimeError("Surface creation failed.") + raise SurfaceCreationError() def delete_surface_on_server(self): """Deletes the surface on server.""" diff --git a/src/ansys/fluent/core/services/datamodel_se.py b/src/ansys/fluent/core/services/datamodel_se.py index ce784b488b3..765dcf192fc 100644 --- a/src/ansys/fluent/core/services/datamodel_se.py +++ b/src/ansys/fluent/core/services/datamodel_se.py @@ -12,6 +12,7 @@ from ansys.api.fluent.v0.variant_pb2 import Variant import ansys.fluent.core as pyfluent from ansys.fluent.core.data_model_cache import DataModelCache +from ansys.fluent.core.exceptions import InvalidArgument from ansys.fluent.core.services.error_handler import catch_grpc_error from ansys.fluent.core.services.interceptors import ( BatchInterceptor, @@ -25,6 +26,27 @@ logger: logging.Logger = logging.getLogger("pyfluent.datamodel") +class InvalidNamedObject(RuntimeError): + """Provides the error when the object is not a named object.""" + + def __init__(self, class_name): + super().__init__(f"{class_name} is not a named object class.") + + +class SubscribeEventError(RuntimeError): + """Provides the error when server fails to subscribe from event.""" + + def __init__(self, request): + super().__init__(f"Failed to subscribe event: {request}!") + + +class UnsubscribeEventError(RuntimeError): + """Provides the error when server fails to unsubscribe from event.""" + + def __init__(self, request): + super().__init__(f"Failed to unsubscribe event: {request}!") + + class Attribute(Enum): """Contains the standard names of data model attributes associated with the data model service.""" @@ -285,12 +307,18 @@ def __init__( service: DatamodelService, request: DataModelProtoModule.SubscribeEventsRequest, ) -> None: - """Subscribe to a datamodel event.""" + """Subscribe to a datamodel event. + + Raises + ------ + SubscribeEventError + If server fails to subscribe from event. + """ self._service = service response = service.subscribe_events(request) response = response.response[0] if response.status != DataModelProtoModule.STATUS_SUBSCRIBED: - raise RuntimeError(f"Failed to subscribe event: {request}!") + raise SubscribeEventError(request) self.status = response.status self.tag = response.tag self._service.events[self.tag] = self @@ -300,7 +328,7 @@ def unsubscribe(self) -> None: Raises ------ - RuntimeError + UnsubscribeEventError If server fails to unsubscribe from event. """ if self.status == DataModelProtoModule.STATUS_SUBSCRIBED: @@ -310,7 +338,7 @@ def unsubscribe(self) -> None: response = self._service.unsubscribe_events(request) response = response.response[0] if response.status != DataModelProtoModule.STATUS_UNSUBSCRIBED: - raise RuntimeError(f"Failed to unsubscribe event: {request}!") + raise UnsubscribeEventError(request) self.status = response.status self._service.events.pop(self.tag, None) @@ -552,15 +580,13 @@ def rename(self, new_name: str) -> None: Raises ------ - RuntimeError + InvalidNamedObject If the object is not a named object. """ try: self._name_.set_state(new_name) except AttributeError: - raise RuntimeError( - f"{self.__class__.__name__} is not a named object class." - ) + raise InvalidNamedObject(self.__class__.__name__) def name(self) -> str: """Get the name of the named object. @@ -572,15 +598,13 @@ def name(self) -> str: Raises ------ - RuntimeError + InvalidNamedObject If the object is not a named object. """ try: return self._name_() except AttributeError: - raise RuntimeError( - f"{self.__class__.__name__} is not a named object class." - ) + raise InvalidNamedObject(self.__class__.__name__) def _raise_method_not_yet_implemented_exception(self) -> NoReturn: raise AttributeError("This method is yet to be implemented in pyfluent.") @@ -1344,13 +1368,13 @@ def get_mode(mode: str) -> "DataModelType": Raises ------ - TypeError + InvalidArgument If an unknown mode is passed. """ for m in DataModelType: if mode in m.value[0]: return m - raise TypeError(f"The specified mode: {mode} was not found.") + raise InvalidArgument(f"The specified mode: {mode} was not found.") class PyMenuGeneric(PyMenu): diff --git a/src/ansys/fluent/core/services/field_data.py b/src/ansys/fluent/core/services/field_data.py index 639f294fea5..4adc8d61e37 100644 --- a/src/ansys/fluent/core/services/field_data.py +++ b/src/ansys/fluent/core/services/field_data.py @@ -8,6 +8,10 @@ from ansys.api.fluent.v0 import field_data_pb2 as FieldDataProtoModule from ansys.api.fluent.v0 import field_data_pb2_grpc as FieldGrpcModule +from ansys.fluent.core.exceptions import ( + DisallowedValuesError, + SurfaceSpecificationError, +) from ansys.fluent.core.services.error_handler import catch_grpc_error from ansys.fluent.core.services.interceptors import ( BatchInterceptor, @@ -206,74 +210,12 @@ def validate_surfaces(self, surfaces: List[str]): _AllowedSurfaceNames(info=self.get_surfaces_info()).valid_name(surface) -def unavailable_field_error_message(context: str, field_name: str) -> str: - """Error message for unavailable fields.""" - return f"{field_name} is not a currently available {context}." - - -class FieldNameError(ValueError): - """Exception class for errors in field name.""" - - pass - - -class ScalarFieldNameError(FieldNameError): - """Exception class for errors in scalar field name.""" - - def __init__(self, field_name: str, allowed_values: List[str]): - """__init__ method of ScalarFieldNameError class.""" - self.field_name = field_name - super().__init__( - allowed_name_error_message("scalar field", field_name, allowed_values) - ) - - -class VectorFieldNameError(FieldNameError): - """Exception class for errors in vector field name.""" - - def __init__(self, field_name: str, allowed_values: List[str]): - """__init__ method of VectorFieldNameError class.""" - self.field_name = field_name - super().__init__( - allowed_name_error_message("vector field", field_name, allowed_values) - ) - - class FieldUnavailable(RuntimeError): - """Exception class for when field is unavailable.""" + """Provides the error when field is unavailable.""" pass -class ScalarFieldUnavailable(FieldUnavailable): - """Exception class for when scalar field is unavailable.""" - - def __init__(self, field_name: str): - """__init__ method of ScalarFieldUnavailable class.""" - self.field_name = field_name - super().__init__(unavailable_field_error_message("scalar field", field_name)) - - -class VectorFieldUnavailable(FieldUnavailable): - """Exception class for when vector field is unavailable.""" - - def __init__(self, field_name: str): - """__init__ method of VectorFieldUnavailable class.""" - self.field_name = field_name - super().__init__(unavailable_field_error_message("vector field", field_name)) - - -class SurfaceNameError(ValueError): - """Exception class for errors in surface name.""" - - def __init__(self, surface_name: str, allowed_values: List[str]): - """__init__ method of SurfaceNameError class.""" - self.surface_name = surface_name - super().__init__( - allowed_name_error_message("surface", surface_name, allowed_values) - ) - - class SurfaceDataType(IntEnum): """Provides surface data types.""" @@ -311,11 +253,14 @@ def valid_name(self, field_name): names = self if not names.is_valid(field_name, respect_data_valid=False): raise self._field_name_error( - field_name=field_name, - allowed_values=names(respect_data_valid=False), + allowed_name_error_message( + "field", field_name, names(respect_data_valid=False) + ) ) if not names.is_valid(field_name, respect_data_valid=True): - raise self._field_unavailable_error(field_name) + raise self._field_unavailable_error( + f"{field_name} is not a currently available field." + ) return field_name @@ -327,10 +272,7 @@ def __call__(self, respect_data_valid: bool = True) -> List[str]: def valid_name(self, surface_name: str) -> str: """Returns valid names.""" if validate_inputs and not self.is_valid(surface_name): - raise SurfaceNameError( - surface_name=surface_name, - allowed_values=self(), - ) + raise DisallowedValuesError("surface", surface_name, self()) return surface_name @@ -346,8 +288,8 @@ def __call__(self, respect_data_valid: bool = True) -> List[int]: class _AllowedScalarFieldNames(_AllowedFieldNames): - _field_name_error = ScalarFieldNameError - _field_unavailable_error = ScalarFieldUnavailable + _field_name_error = DisallowedValuesError + _field_unavailable_error = FieldUnavailable def __call__(self, respect_data_valid: bool = True) -> List[str]: field_dict = ( @@ -365,8 +307,8 @@ def __call__(self, respect_data_valid: bool = True) -> List[str]: class _AllowedVectorFieldNames(_AllowedFieldNames): - _field_name_error = VectorFieldNameError - _field_unavailable_error = VectorFieldUnavailable + _field_name_error = DisallowedValuesError + _field_unavailable_error = FieldUnavailable def __call__(self, respect_data_valid: bool = True) -> List[str]: return ( @@ -756,9 +698,14 @@ def _get_surface_ids( Returns ------- List[int] + + Raises + ------ + SurfaceSpecificationError + If both ``surface_ids`` and ``surface_names`` are provided. """ if surface_ids and (surface_name or surface_names): - raise RuntimeError("Please provide either surface names or surface IDs.") + raise SurfaceSpecificationError() if not surface_ids: surface_ids = [] if surface_names: @@ -771,7 +718,7 @@ def _get_surface_ids( allowed_surface_names.valid_name(surface_name) ]["surface_id"] else: - raise RuntimeError("Please provide either surface names or surface IDs.") + raise SurfaceSpecificationError() return surface_ids diff --git a/src/ansys/fluent/core/services/meshing_queries.py b/src/ansys/fluent/core/services/meshing_queries.py index 61ae1f53007..315feb0b081 100644 --- a/src/ansys/fluent/core/services/meshing_queries.py +++ b/src/ansys/fluent/core/services/meshing_queries.py @@ -6,6 +6,7 @@ from ansys.api.fluent.v0 import meshing_queries_pb2 as MeshingQueriesProtoModule from ansys.api.fluent.v0 import meshing_queries_pb2_grpc as MeshingQueriesGrpcModule +from ansys.fluent.core.exceptions import DisallowedValuesError from ansys.fluent.core.services.error_handler import catch_grpc_error from ansys.fluent.core.services.interceptors import ( BatchInterceptor, @@ -622,12 +623,12 @@ def __init__(self, service: MeshingQueriesService): def __get_allowed_region_type(self, region_type): """Check region_type in available regions.""" if region_type not in self.region_types: - raise ValueError(f"Allowed region types - {self.region_types}\n") + raise DisallowedValuesError("region-type", region_type, self.region_types) def _get_allowed_orders(self, order): """Check order in available orders.""" if order not in self.orders: - raise ValueError(f"Allowed orders - {self.orders}\n") + raise DisallowedValuesError("order", order, self.orders) def get_all_object_name_list(self) -> Any: """Return a list of all objects. @@ -646,9 +647,9 @@ def _get_allowed_object(self, object): if isinstance(object, list): for obj in object: if obj not in allowed_args: - raise ValueError(f"Allowed objects - {allowed_args}\n") + raise DisallowedValuesError("object", obj, allowed_args) elif isinstance(object, str) and object not in allowed_args: - raise ValueError(f"Allowed objects - {allowed_args}\n") + raise DisallowedValuesError("object", object, allowed_args) def get_region_name_list_of_object(self, object) -> Any: """Return a list of regions in the specified object. @@ -672,9 +673,9 @@ def _get_allowed_region(self, region): if isinstance(region, list): for reg in region: if reg not in regions: - raise ValueError(f"Allowed regions - {regions}\n") + raise DisallowedValuesError("region", reg, regions) elif isinstance(region, str) and region not in regions: - raise ValueError(f"Allowed regions - {regions}\n") + raise DisallowedValuesError("region", region, regions) def get_face_zone_at_location(self, location) -> Any: """Return face zone at or closest to a specified location. diff --git a/src/ansys/fluent/core/solver/error_message.py b/src/ansys/fluent/core/solver/error_message.py index 5e7938c60f7..d437f5045a2 100644 --- a/src/ansys/fluent/core/solver/error_message.py +++ b/src/ansys/fluent/core/solver/error_message.py @@ -1,6 +1,6 @@ import difflib from functools import partial -from typing import List +from typing import Any, List def closest_allowed_names(trial_name: str, allowed_names: str) -> List[str]: @@ -10,14 +10,20 @@ def closest_allowed_names(trial_name: str, allowed_names: str) -> List[str]: def allowed_name_error_message( - context: str, trial_name: str, allowed_values: List[str] + context: str, trial_name: str, allowed_values: Any ) -> str: - """Provide the closest names matching the 'trial_name' from the 'allowed_values' - list.""" + """Provide an error message with the closest names matching the 'trial_name' from + the 'allowed_values' list.""" message = f"{trial_name} is not an allowed {context} name.\n" - matches = closest_allowed_names(trial_name, allowed_values) - if matches: - message += f"The most similar names are: {', '.join(matches)}." + matches = None + if allowed_values: + if isinstance(allowed_values, list) and isinstance(allowed_values[0], str): + matches = closest_allowed_names(trial_name, allowed_values) + if matches: + message += f"The most similar names are: {', '.join(matches)}." + else: + message += f"The allowed values are: {allowed_values}." + return message diff --git a/src/ansys/fluent/core/solver/flobject.py b/src/ansys/fluent/core/solver/flobject.py index 7e0bbaf4678..c33cc8dcc0c 100644 --- a/src/ansys/fluent/core/solver/flobject.py +++ b/src/ansys/fluent/core/solver/flobject.py @@ -32,6 +32,13 @@ settings_logger = logging.getLogger("pyfluent.settings_api") +class InactiveObjectError(RuntimeError): + """Provides the error when the object is inactive.""" + + def __init__(self): + super().__init__("Object is not active.") + + class _InlineConstants: is_active = "active?" is_read_only = "read-only?" @@ -177,14 +184,14 @@ def get_attr( Raises ------ - RuntimeError + InactiveObjectError If any attribute other than ``"active?`` is queried when the object is not active. """ attrs = self.get_attrs([attr]) if attrs: attrs = attrs.get("attrs", attrs) if attr != "active?" and attrs and attrs.get("active?", True) is False: - raise RuntimeError("Object is not active") + raise InactiveObjectError() val = None if attrs: val = attrs[attr] diff --git a/src/ansys/fluent/core/streaming_services/events_streaming.py b/src/ansys/fluent/core/streaming_services/events_streaming.py index 6ecc5922d28..9cdd5921293 100644 --- a/src/ansys/fluent/core/streaming_services/events_streaming.py +++ b/src/ansys/fluent/core/streaming_services/events_streaming.py @@ -1,9 +1,10 @@ """Module for events management.""" from functools import partial import logging -from typing import Callable, List, Optional +from typing import Callable, List from ansys.api.fluent.v0 import events_pb2 as EventsProtoModule +from ansys.fluent.core.exceptions import DisallowedValuesError, InvalidArgument from ansys.fluent.core.streaming_services.streaming import StreamingService network_logger = logging.getLogger("pyfluent.networking") @@ -58,8 +59,8 @@ def _process_streaming(self, id, stream_begin_method, started_evt, *args, **kwar self._fluent_error_state.set("fatal", error_message) continue callbacks_map = self._service_callbacks.get(event_name, {}) - for call_back in callbacks_map.values(): - call_back( + for callback in callbacks_map.values(): + callback( session_id=self._session_id, event_info=getattr(response, event_name), ) @@ -68,8 +69,8 @@ def _process_streaming(self, id, stream_begin_method, started_evt, *args, **kwar def register_callback( self, - event_name: Optional[str] = None, - call_back: Optional[Callable] = None, + event_name: str, + callback: Callable, *args, **kwargs, ): @@ -80,7 +81,7 @@ def register_callback( event_name : str Event name to register the callback to. - call_back : Callable + callback : Callable Callback to register. Returns @@ -90,25 +91,25 @@ def register_callback( Raises ------ - RuntimeError + InvalidArgument If event name is not valid. + DisallowedValuesError + If an argument value not in allowed values. """ - if event_name is None or call_back is None: - raise RuntimeError( - "Please provide compulsory arguments : 'event_name' and 'call_back'" - ) + if event_name is None or callback is None: + raise InvalidArgument("'event_name' and 'callback' ") if event_name not in self.events_list: - raise RuntimeError(f"{event_name} is not a valid event.") + raise DisallowedValuesError("event-name", event_name, self.events_list) with self._lock: event_name = event_name.lower() callback_id = f"{event_name}-{next(self._service_callback_id)}" callbacks_map = self._service_callbacks.get(event_name) if callbacks_map: - callbacks_map.update({callback_id: partial(call_back, *args, **kwargs)}) + callbacks_map.update({callback_id: partial(callback, *args, **kwargs)}) else: self._service_callbacks[event_name] = { - callback_id: partial(call_back, *args, **kwargs) + callback_id: partial(callback, *args, **kwargs) } def unregister_callback(self, callback_id: str): diff --git a/src/ansys/fluent/core/streaming_services/streaming.py b/src/ansys/fluent/core/streaming_services/streaming.py index fccb178ec37..e83cf1ea068 100644 --- a/src/ansys/fluent/core/streaming_services/streaming.py +++ b/src/ansys/fluent/core/streaming_services/streaming.py @@ -29,12 +29,12 @@ def is_streaming(self): with self._lock: return self._streaming - def register_callback(self, call_back: Callable, *args, **kwargs) -> str: + def register_callback(self, callback: Callable, *args, **kwargs) -> str: """Register the callback. Parameters ---------- - call_back : Callable + callback : Callable Callback to register. Returns @@ -44,7 +44,7 @@ def register_callback(self, call_back: Callable, *args, **kwargs) -> str: """ with self._lock: callback_id = f"{next(self._service_callback_id)}" - self._service_callbacks[callback_id] = [call_back, args, kwargs] + self._service_callbacks[callback_id] = [callback, args, kwargs] return callback_id def unregister_callback(self, callback_id: str): diff --git a/src/ansys/fluent/core/utils/data_transfer.py b/src/ansys/fluent/core/utils/data_transfer.py index 9c2fce3c611..92e0a32baee 100644 --- a/src/ansys/fluent/core/utils/data_transfer.py +++ b/src/ansys/fluent/core/utils/data_transfer.py @@ -15,6 +15,13 @@ network_logger = logging.getLogger("pyfluent.networking") +class MeshWriteError(RuntimeError): + """Provides the error when mesh write is unsuccessful.""" + + def __init__(self): + super().__init__("Could not write mesh from meshing session.") + + @asynchronous def _read_case_into(solver, file_type, file_name, full_file_name_container=None): network_logger.info(f"Trying to read case: {file_name}") @@ -80,7 +87,7 @@ def transfer_case( Raises ------ - RuntimeError + MeshWriteError If mesh cannot be written from ``source_instance``. """ inside_container = source_instance.connection_properties.inside_container @@ -160,4 +167,4 @@ def transfer_case( f"Encountered exception while cleaning up during case transfer {ex}" ) return - raise RuntimeError("Could not write mesh from meshing session.") + raise MeshWriteError() diff --git a/src/ansys/fluent/core/utils/execution.py b/src/ansys/fluent/core/utils/execution.py index 62b2211222b..b2ce82eb296 100644 --- a/src/ansys/fluent/core/utils/execution.py +++ b/src/ansys/fluent/core/utils/execution.py @@ -6,6 +6,8 @@ import time from typing import Any, Callable, Optional +from ansys.fluent.core.exceptions import InvalidArgument + def asynchronous(f: Callable) -> Callable: """Use for decorating functions that are to execute asynchronously. The decorated @@ -122,7 +124,7 @@ def timeout_loop( Raises ------ - RuntimeError + InvalidArgument If an unrecognized value is passed for ``expected``. Examples @@ -160,9 +162,7 @@ def _exec(*_args, **_kwargs): if not ret_obj: return ret_obj else: - raise RuntimeError( - "Unrecognized value for 'expected' variable. Accepted: 'truthy' or 'falsy'." - ) + raise InvalidArgument("Specify 'expected' as either 'truthy' or 'falsy'.") time.sleep(idle_period) time_elapsed += idle_period diff --git a/tests/test_casereader.py b/tests/test_casereader.py index 4b0cc6b8e4b..611915de921 100644 --- a/tests/test_casereader.py +++ b/tests/test_casereader.py @@ -211,7 +211,6 @@ def test_case_reader_get_rp_and_config_vars(): with pytest.raises(RuntimeError) as msg: reader.rp_var.defaults.pre_r19__dot0_early() - assert msg.value.args[0] == r"Invalid variable defaults/pre-r19.0-early" with pytest.raises(ValueError) as msg: reader.config_var("rp-3d") diff --git a/tests/test_field_data.py b/tests/test_field_data.py index 82b3376193e..e45f48ea28d 100644 --- a/tests/test_field_data.py +++ b/tests/test_field_data.py @@ -3,13 +3,8 @@ from util.solver_workflow import new_solver_session # noqa: F401 from ansys.fluent.core import examples -from ansys.fluent.core.services.field_data import ( - ScalarFieldNameError, - ScalarFieldUnavailable, - SurfaceDataType, - SurfaceNameError, - VectorFieldNameError, -) +from ansys.fluent.core.exceptions import DisallowedValuesError +from ansys.fluent.core.services.field_data import FieldUnavailable, SurfaceDataType HOT_INLET_TEMPERATURE = 313.15 @@ -306,23 +301,20 @@ def test_field_data_errors(new_solver_session) -> None: "mixing_elbow.msh.h5", "pyfluent/mixing_elbow" ) - with pytest.raises(ScalarFieldNameError) as fne: + with pytest.raises(DisallowedValuesError) as fne: solver.field_data.get_scalar_field_data( field_name="y-face-area", surface_ids=[0] ) - assert fne.value.field_name == "y-face-area" - with pytest.raises(ScalarFieldNameError) as fne: + with pytest.raises(DisallowedValuesError) as fne: solver.field_data.get_scalar_field_data( field_name="partition-neighbors", surface_ids=[0] ) - assert fne.value.field_name == "partition-neighbors" solver.file.read(file_type="case", file_name=import_file_name) - with pytest.raises(ScalarFieldUnavailable) as fnu: + with pytest.raises(FieldUnavailable) as fnu: solver.field_data.get_scalar_field_data(field_name="density", surface_ids=[0]) - assert fnu.value.field_name == "density" y_face_area = solver.field_data.get_scalar_field_data( field_name="y-face-area", surface_ids=[0] @@ -340,15 +332,13 @@ def test_field_data_errors(new_solver_session) -> None: # Get field data object field_data = solver.field_data - with pytest.raises(SurfaceNameError) as sne: + with pytest.raises(DisallowedValuesError) as sne: solver.field_data.get_scalar_field_data( field_name="density", surface_name="bob" ) - assert sne.value.surface_name == "bob" - with pytest.raises(ScalarFieldNameError) as fne: + with pytest.raises(DisallowedValuesError) as fne: solver.field_data.get_scalar_field_data(field_name="xdensity", surface_ids=[0]) - assert fne.value.field_name == "xdensity" @pytest.mark.fluent_version(">=23.2") @@ -363,20 +353,17 @@ def test_field_info_validators(new_solver_session) -> None: vector_field_1 = solver.field_info.validate_vector_fields("velocity") assert vector_field_1 is None - with pytest.raises(VectorFieldNameError) as vector_field_error: + with pytest.raises(DisallowedValuesError) as vector_field_error: solver.field_info.validate_vector_fields("relative-vel") - assert vector_field_error.value.field_name == "relative-vel" scalar_field_1 = solver.field_info.validate_scalar_fields("z-velocity") assert scalar_field_1 is None - with pytest.raises(ScalarFieldNameError) as scalar_field_error: + with pytest.raises(DisallowedValuesError) as scalar_field_error: solver.field_info.validate_scalar_fields("z-vel") - assert scalar_field_error.value.field_name == "z-vel" surface = solver.field_info.validate_surfaces(["cold-inlet"]) assert surface is None - with pytest.raises(SurfaceNameError) as surface_error: + with pytest.raises(DisallowedValuesError) as surface_error: solver.field_info.validate_surfaces(["out"]) - assert surface_error.value.surface_name == "out" diff --git a/tests/test_file_session.py b/tests/test_file_session.py index b80c2b71a6e..c9ed9792f82 100644 --- a/tests/test_file_session.py +++ b/tests/test_file_session.py @@ -3,7 +3,12 @@ import pytest from ansys.fluent.core import examples -from ansys.fluent.core.file_session import FileSession +from ansys.fluent.core.exceptions import SurfaceSpecificationError +from ansys.fluent.core.file_session import ( + FileSession, + InvalidFieldName, + InvalidMultiPhaseFieldName, +) from ansys.fluent.core.services.field_data import SurfaceDataType @@ -328,13 +333,18 @@ def test_error_handling_multi_phase(): field_data = file_session.field_data transaction_1 = field_data.new_transaction() - error_message = ( - r"For multi-phase cases field name should have a prefix of phase name." - ) - with pytest.raises(RuntimeError) as msg: + with pytest.raises(InvalidMultiPhaseFieldName) as msg: transaction_1.add_scalar_fields_request("SV_WALL_YPLUS", [29, 30]) - assert msg.value.args[0] == error_message - with pytest.raises(RuntimeError) as msg: + with pytest.raises(InvalidMultiPhaseFieldName) as msg: d_size = field_data.get_vector_field_data("velocity", surface_ids=[34])[34].size - assert msg.value.args[0] == error_message + + with pytest.raises(InvalidFieldName) as msg: + d_size = field_data.get_vector_field_data( + "phase-1:temperature", surface_ids=[34] + )[34].size + + with pytest.raises(SurfaceSpecificationError) as msg: + d_size = field_data.get_vector_field_data( + "velocity", surface_ids=[34], surface_name="wall" + )[34].size diff --git a/tests/test_flobject.py b/tests/test_flobject.py index be63acc87bb..71e85a05aa2 100644 --- a/tests/test_flobject.py +++ b/tests/test_flobject.py @@ -10,7 +10,7 @@ from ansys.fluent.core.examples import download_file from ansys.fluent.core.solver import flobject -from ansys.fluent.core.solver.flobject import find_children +from ansys.fluent.core.solver.flobject import InactiveObjectError, find_children class Setting: @@ -507,9 +507,8 @@ def test_attrs(): assert r.g_1.s_4.get_attr("allowed-values") == ["foo", "bar"] r.g_1.b_3 = True assert not r.g_1.s_4.get_attr("active?") - with pytest.raises(RuntimeError) as einfo: + with pytest.raises(InactiveObjectError) as einfo: r.g_1.s_4.get_attr("allowed-values") == ["foo", "bar"] - assert einfo.value.args == ("Object is not active",) # The following test is commented out as codegen module is not packaged in the diff --git a/tests/test_fluent_session.py b/tests/test_fluent_session.py index 871b68aee51..a90ac68f16c 100644 --- a/tests/test_fluent_session.py +++ b/tests/test_fluent_session.py @@ -14,7 +14,8 @@ import ansys.fluent.core as pyfluent from ansys.fluent.core.examples import download_file -from ansys.fluent.core.fluent_connection import get_container +from ansys.fluent.core.fluent_connection import WaitTypeError, get_container +from ansys.fluent.core.launcher.launcher import IpPortNotProvided from ansys.fluent.core.utils.execution import asynchronous, timeout_loop @@ -124,6 +125,13 @@ def test_does_not_exit_fluent_by_default_when_connected_to_running_fluent( monkeypatch, ) -> None: session1 = pyfluent.launch_fluent() + + with pytest.raises(IpPortNotProvided) as msg: + session2 = pyfluent.connect_to_fluent( + ip=session1.connection_properties.ip, + password=session1.connection_properties.password, + ) + session2 = pyfluent.connect_to_fluent( ip=session1.connection_properties.ip, port=session1.connection_properties.port, @@ -248,3 +256,7 @@ def test_fluent_exit_wait(): session3 = pyfluent.launch_fluent() session3.exit(wait=True) assert session3.fluent_connection.wait_process_finished(wait=0) + + with pytest.raises(WaitTypeError) as msg: + session4 = pyfluent.launch_fluent() + session4.exit(wait="wait") diff --git a/tests/test_launcher.py b/tests/test_launcher.py index 1362336aae3..7c2d2a093b0 100644 --- a/tests/test_launcher.py +++ b/tests/test_launcher.py @@ -4,49 +4,62 @@ import pytest import ansys.fluent.core as pyfluent +from ansys.fluent.core.exceptions import DisallowedValuesError, InvalidArgument from ansys.fluent.core.launcher import launcher from ansys.fluent.core.launcher.launcher import ( + AnsysVersionNotFound, + DockerContainerLaunchNotSupported, LaunchFluentError, + UnexpectedKeywordArgument, + check_docker_support, get_ansys_version, get_fluent_exe_path, ) +def test_mode(): + with pytest.raises(DisallowedValuesError) as msg: + pyfluent.launch_fluent( + mode="meshing-solver", + start_container=False, + ) + + +@pytest.mark.skip(reason="Can be used only locally.") @pytest.mark.standalone def test_unsuccessful_fluent_connection(): # start-timeout is intentionally provided to be 2s for the connection to fail - with pytest.raises(RuntimeError) as msg: + with pytest.raises(TimeoutError) as msg: pyfluent.launch_fluent(mode="solver", start_timeout=2) - assert msg.value.args[0] == "The launch process has been timed out." def test_additional_argument_g_gu(): default_windows_flag = launcher._is_windows() launcher._is_windows = lambda: True try: - with pytest.raises(ValueError) as msg: + with pytest.raises(InvalidArgument) as msg: pyfluent.launch_fluent( mode="solver", show_gui=True, additional_arguments="-g", start_container=False, ) - assert ( - msg.value.args[0] == "'-g' and '-gu' is not supported on windows platform." - ) - - with pytest.raises(ValueError) as msg: + with pytest.raises(InvalidArgument) as msg: pyfluent.launch_fluent( mode="solver", additional_arguments="-gu", start_container=False ) - assert ( - msg.value.args[0] == "'-g' and '-gu' is not supported on windows platform." - ) finally: launcher._is_windows = lambda: default_windows_flag def test_container_launcher(): + if not check_docker_support(): + with pytest.raises(DockerContainerLaunchNotSupported) as msg: + container_dict_1 = pyfluent.launch_fluent(start_container=True) + container_dict_2 = pyfluent.launch_fluent( + start_container=True, dry_run=True + ) + # test dry_run container_dict = pyfluent.launch_fluent(start_container=True, dry_run=True) assert isinstance(container_dict, dict) @@ -84,9 +97,9 @@ def test_gpu_launch_arg_additional_arg(monkeypatch): def test_kwargs(): - with pytest.raises(RuntimeError): + with pytest.raises(UnexpectedKeywordArgument): pyfluent.launch_fluent(abc=1, meshing_mode=True) - with pytest.raises(TypeError): + with pytest.raises(UnexpectedKeywordArgument): pyfluent.launch_fluent(abc=1, xyz=2) @@ -96,9 +109,9 @@ def test_get_fluent_exe_path_when_nothing_is_set(monkeypatch): monkeypatch.delenv("AWP_ROOT232", raising=False) monkeypatch.delenv("AWP_ROOT231", raising=False) monkeypatch.delenv("AWP_ROOT222", raising=False) - with pytest.raises(RuntimeError): + with pytest.raises(AnsysVersionNotFound): get_ansys_version() - with pytest.raises(RuntimeError): + with pytest.raises(AnsysVersionNotFound): get_fluent_exe_path() diff --git a/tests/test_launcher_remote.py b/tests/test_launcher_remote.py index fad62129b43..2428aaae7af 100644 --- a/tests/test_launcher_remote.py +++ b/tests/test_launcher_remote.py @@ -1,12 +1,23 @@ """Test the PyPIM integration.""" +from concurrent import futures import os from unittest.mock import create_autospec import grpc +from grpc_health.v1 import health_pb2_grpc +import pytest +from test_session import MockHealthServicer, MockSchemeEvalServicer from util.solver_workflow import new_solver_session # noqa: F401 +from ansys.api.fluent.v0 import scheme_eval_pb2_grpc +from ansys.fluent.core.fluent_connection import ( + FluentConnection, + UnsupportedRemoteFluentInstance, +) from ansys.fluent.core.launcher import launcher +from ansys.fluent.core.session import BaseSession import ansys.fluent.core.utils.fluent_version as docker_image_version +from ansys.fluent.core.utils.networking import get_free_port import ansys.platform.instancemanagement as pypim @@ -64,3 +75,26 @@ def test_launch_remote_instance(monkeypatch, new_solver_session): # and it kept track of the instance to be able to delete it assert fluent.fluent_connection._remote_instance == mock_instance + + server = grpc.server(futures.ThreadPoolExecutor(max_workers=1)) + ip = "127.0.0.1" + port = get_free_port() + server.add_insecure_port(f"{ip}:{port}") + health_pb2_grpc.add_HealthServicer_to_server(MockHealthServicer(), server) + scheme_eval_pb2_grpc.add_SchemeEvalServicer_to_server( + MockSchemeEvalServicer(), server + ) + server.start() + + with pytest.raises(UnsupportedRemoteFluentInstance) as msg: + session = BaseSession( + FluentConnection( + ip=ip, + port=port, + password="12345", + remote_instance=mock_instance, + cleanup_on_exit=False, + ) + ) + session.exit(wait=60) + session.fluent_connection.wait_process_finished(wait=60) diff --git a/tests/test_meshing_workflow.py b/tests/test_meshing_workflow.py index 4df415c903f..d17aeccdfe1 100644 --- a/tests/test_meshing_workflow.py +++ b/tests/test_meshing_workflow.py @@ -289,11 +289,9 @@ def test_read_only_behaviour_of_command_arguments(new_mesh_session): with pytest.raises(AttributeError) as msg: import_geom.arguments.MeshUnit.set_state("in") - assert msg.value.args[0] == "Command Arguments are read-only." with pytest.raises(AttributeError) as msg: import_geom.arguments.CadImportOptions.OneZonePer.set_state(None) - assert msg.value.args[0] == "Command Arguments are read-only." assert "set_state" in dir(m()) assert "set_state" in dir(m().NumParts) @@ -328,16 +326,12 @@ def test_dummy_journal_data_model_methods(new_mesh_session): with pytest.raises(AttributeError) as msg: import_geom.delete_child() - assert msg.value.args[0] == "This method is yet to be implemented in pyfluent." with pytest.raises(AttributeError) as msg: import_geom.delete_child_objects() - assert msg.value.args[0] == "This method is yet to be implemented in pyfluent." with pytest.raises(AttributeError) as msg: import_geom.delete_all_child_objects() - assert msg.value.args[0] == "This method is yet to be implemented in pyfluent." with pytest.raises(AttributeError) as msg: import_geom.fix_state() - assert msg.value.args[0] == "This method is yet to be implemented in pyfluent." @pytest.mark.fluent_version(">=23.1") diff --git a/tests/test_reduction.py b/tests/test_reduction.py index 6ae667796b9..a0808e7333b 100644 --- a/tests/test_reduction.py +++ b/tests/test_reduction.py @@ -235,8 +235,6 @@ def _test_error_handling(solver): locations=solver.setup.boundary_conditions.velocity_inlet, ) - assert msg.value.args[0] == "Unable to evaluate expression" - def _test_force(solver): solver.solution.initialization.hybrid_initialize() diff --git a/tests/test_session.py b/tests/test_session.py index 2674c67ccfa..3832044887a 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -14,7 +14,7 @@ from ansys.api.fluent.v0.scheme_pointer_pb2 import SchemePointer import ansys.fluent.core as pyfluent from ansys.fluent.core import connect_to_fluent, examples, session -from ansys.fluent.core.fluent_connection import FluentConnection +from ansys.fluent.core.fluent_connection import FluentConnection, PortNotProvided from ansys.fluent.core.launcher.launcher import LaunchFluentError from ansys.fluent.core.session import BaseSession from ansys.fluent.core.utils.execution import timeout_loop @@ -80,6 +80,12 @@ def test_create_session_by_passing_ip_and_port_and_password() -> None: MockSchemeEvalServicer(), server ) server.start() + + with pytest.raises(PortNotProvided) as msg: + session = BaseSession( + FluentConnection(ip=ip, password="12345", cleanup_on_exit=False) + ) + session = BaseSession( FluentConnection(ip=ip, port=port, password="12345", cleanup_on_exit=False) ) @@ -218,7 +224,9 @@ def test_create_session_from_launch_fluent_by_setting_ip_and_port_env_var( server.start() monkeypatch.setenv("PYFLUENT_FLUENT_IP", ip) monkeypatch.setenv("PYFLUENT_FLUENT_PORT", str(port)) - session = connect_to_fluent(cleanup_on_exit=False, password="12345") + session = connect_to_fluent( + cleanup_on_exit=False, ip=ip, port=port, password="12345" + ) # check a few dir elements session_dir = dir(session) for attr in ("field_data", "field_info"): diff --git a/tests/test_settings_reader.py b/tests/test_settings_reader.py index 51b7b4f08ce..a038e1cea9a 100644 --- a/tests/test_settings_reader.py +++ b/tests/test_settings_reader.py @@ -61,7 +61,6 @@ def test_meshing_unavailable(): reader = SettingsReader(settings_file_name=static_mixer_settings_file()) with pytest.raises(AttributeError) as msg: reader.get_mesh() - assert msg.value.args[0] == "'SettingsFile' object has no attribute 'get_mesh'" def test_settings_reader_get_rp_and_config_vars(): @@ -79,7 +78,6 @@ def test_settings_reader_get_rp_and_config_vars(): with pytest.raises(RuntimeError) as msg: reader.rp_var.defaults.pre_r19__dot0_early() - assert msg.value.args[0] == r"Invalid variable defaults/pre-r19.0-early" with pytest.raises(ValueError) as msg: reader.config_var("rp-3d") diff --git a/tests/test_utils.py b/tests/test_utils.py index 1bc8861fc03..8b38b298215 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,12 @@ import time -from ansys.fluent.core.utils.execution import timeout_exec, timeout_loop +import pytest + +from ansys.fluent.core.utils.execution import ( + InvalidArgument, + timeout_exec, + timeout_loop, +) def test_timeout_exec(): @@ -47,6 +53,9 @@ def __call__(self): ret = timeout_loop(waiter, timeout=0.2, expected="truthy", idle_period=0.1) assert ret is False + with pytest.raises(InvalidArgument) as msg: + timeout_loop(waiter, timeout=0.2, expected=True, idle_period=0.1) + def count_key_recursive(dictionary, key): count = 0