diff --git a/.github/workflows/scripts/separate_long_core_tests.ps1 b/.github/workflows/scripts/separate_long_core_tests.ps1 index 16db460dc9..096b90cd11 100644 --- a/.github/workflows/scripts/separate_long_core_tests.ps1 +++ b/.github/workflows/scripts/separate_long_core_tests.ps1 @@ -5,6 +5,7 @@ New-Item -Path ".\" -Name "test_multi_server" -ItemType "directory" New-Item -Path ".\" -Name "test_workflow" -ItemType "directory" New-Item -Path ".\" -Name "test_remote_workflow" -ItemType "directory" New-Item -Path ".\" -Name "test_remote_operator" -ItemType "directory" +New-Item -Path ".\" -Name "test_service" -ItemType "directory" Copy-Item -Path "tests\conftest.py" -Destination ".\test_launcher\" Copy-Item -Path "tests\conftest.py" -Destination ".\test_server\" Copy-Item -Path "tests\conftest.py" -Destination ".\test_local_server\" @@ -12,6 +13,7 @@ Copy-Item -Path "tests\conftest.py" -Destination ".\test_multi_server\" Copy-Item -Path "tests\conftest.py" -Destination ".\test_workflow\" Copy-Item -Path "tests\conftest.py" -Destination ".\test_remote_workflow\" Copy-Item -Path "tests\conftest.py" -Destination ".\test_remote_operator\" +Copy-Item -Path "tests\conftest.py" -Destination ".\test_service\" Copy-Item -Path "tests\test_launcher.py" -Destination ".\test_launcher\" Copy-Item -Path "tests\test_server.py" -Destination ".\test_server\" Copy-Item -Path "tests\test_local_server.py" -Destination ".\test_local_server\" @@ -19,10 +21,12 @@ Copy-Item -Path "tests\test_multi_server.py" -Destination ".\test_multi_server\" Copy-Item -Path "tests\test_workflow.py" -Destination ".\test_workflow\" Copy-Item -Path "tests\test_remote_workflow.py" -Destination ".\test_remote_workflow\" Copy-Item -Path "tests\test_remote_operator.py" -Destination ".\test_remote_operator\" +Copy-Item -Path "tests\test_service.py" -Destination ".\test_service\" Remove-Item -Path "tests\test_server.py" Remove-Item -Path "tests\test_launcher.py" Remove-Item -Path "tests\test_local_server.py" Remove-Item -Path "tests\test_multi_server.py" Remove-Item -Path "tests\test_workflow.py" Remove-Item -Path "tests\test_remote_workflow.py" -Remove-Item -Path "tests\test_remote_operator.py" \ No newline at end of file +Remove-Item -Path "tests\test_remote_operator.py" +Remove-Item -Path "tests\test_service.py" \ No newline at end of file diff --git a/.github/workflows/test_docker.yml b/.github/workflows/test_docker.yml index 6ca1dcb3e8..b4ceed0022 100644 --- a/.github/workflows/test_docker.yml +++ b/.github/workflows/test_docker.yml @@ -59,7 +59,6 @@ jobs: working-directory: test_launcher run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results2.xml --reruns 2 . - if: always() timeout-minutes: 20 - name: "Test API test_server" @@ -67,7 +66,6 @@ jobs: working-directory: test_server run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results4.xml --reruns 2 . - if: always() timeout-minutes: 10 - name: "Test API test_local_server" @@ -75,7 +73,6 @@ jobs: working-directory: test_local_server run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results5.xml --reruns 2 . - if: always() timeout-minutes: 20 - name: "Test API test_multi_server" @@ -104,15 +101,20 @@ jobs: working-directory: test_workflow run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results3.xml --reruns 3 . - if: always() timeout-minutes: 20 + - name: "Test API test_service" + shell: bash + working-directory: test_service + run: | + pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results9.xml --reruns 3 . + - name: "Upload Test Results" uses: actions/upload-artifact@v3 with: name: ${{ env.PACKAGE_NAME }}_${{ matrix.python-version }}_${{ matrix.os }}_pytest_${{ env.ANSYS_VERSION }}_docker path: tests/junit/test-results.xml - if: always() + timeout-minutes: 20 - name: "Upload coverage to Codecov" uses: codecov/codecov-action@v3 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b59b3f55c8..2bc07e05cf 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -133,7 +133,6 @@ jobs: working-directory: test_launcher run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results2.xml --reruns 2 . - if: always() - name: "Kill all servers" uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 @@ -155,7 +154,6 @@ jobs: working-directory: test_local_server run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results5.xml --reruns 2 . - if: always() - name: "Kill all servers" uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 @@ -166,7 +164,6 @@ jobs: working-directory: test_multi_server run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results6.xml --reruns 2 . - if: always() - name: "Kill all servers" uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 @@ -177,7 +174,6 @@ jobs: working-directory: test_remote_workflow run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results7.xml --reruns 2 . - if: always() - name: "Kill all servers" uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 @@ -188,7 +184,6 @@ jobs: working-directory: test_remote_operator run: | pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results8.xml --reruns 2 . - if: always() - name: "Kill all servers" uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 @@ -205,6 +200,16 @@ jobs: uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 if: always() + - name: "Test API test_service" + shell: bash + working-directory: test_service + run: | + pytest $DEBUG --cov=ansys.dpf.core --cov-report=xml --cov-report=html --cov-append --log-level=ERROR --junitxml=../tests/junit/test-results9.xml --reruns 3 . + + - name: "Kill all servers" + uses: pyansys/pydpf-actions/kill-dpf-servers@v2.2 + if: always() + - name: "Upload Test Results" uses: actions/upload-artifact@v3 with: diff --git a/ansys/dpf/core/__init__.py b/ansys/dpf/core/__init__.py index 3f3fb732ba..c4e3947691 100644 --- a/ansys/dpf/core/__init__.py +++ b/ansys/dpf/core/__init__.py @@ -81,7 +81,7 @@ from ansys.dpf.core import path_utilities from ansys.dpf.core import settings from ansys.dpf.core.server_factory import ServerConfig, AvailableServerConfigs -from ansys.dpf.core.server_context import apply_server_context, AvailableServerContexts +from ansys.dpf.core.server_context import set_default_server_context, AvailableServerContexts # for matplotlib # solves "QApplication: invalid style override passed, ignoring it." diff --git a/ansys/dpf/core/core.py b/ansys/dpf/core/core.py index 3344baadd6..ae6b4615a8 100644 --- a/ansys/dpf/core/core.py +++ b/ansys/dpf/core/core.py @@ -439,12 +439,12 @@ def apply_context(self, context): if not self._server().meet_version("6.0"): raise errors.DpfVersionNotSupported("6.0") if self._server().has_client(): - error = self._api.data_processing_apply_context_on_client( - self._server().client, context.context_type.value, context.xml_path + self._api.data_processing_apply_context_on_client( + self._server().client, int(context.licensing_context_type), context.xml_path ) else: - error = self._api.data_processing_apply_context( - context.context_type.value, context.xml_path + self._api.data_processing_apply_context( + int(context.licensing_context_type), context.xml_path ) def initialize_with_context(self, context): @@ -464,14 +464,14 @@ def initialize_with_context(self, context): if self._server().has_client(): if not self._server().meet_version("6.0"): raise errors.DpfVersionNotSupported("6.0") - error = self._api.data_processing_initialize_with_context_on_client( - self._server().client, context.context_type.value, context.xml_path + self._api.data_processing_initialize_with_context_on_client( + self._server().client, int(context.licensing_context_type), context.xml_path ) else: if not self._server().meet_version("4.0"): raise errors.DpfVersionNotSupported("4.0") - error = self._api.data_processing_initialize_with_context( - context.context_type.value, context.xml_path + self._api.data_processing_initialize_with_context( + int(context.licensing_context_type), context.xml_path ) @version_requires("6.0") diff --git a/ansys/dpf/core/custom_operator.py b/ansys/dpf/core/custom_operator.py index 750fe086f2..a8af8616a6 100644 --- a/ansys/dpf/core/custom_operator.py +++ b/ansys/dpf/core/custom_operator.py @@ -19,6 +19,7 @@ operator_specification, dpf_operator, collection, + AvailableServerContexts, ) from ansys.dpf.core._custom_operators_helpers import __operator_main__, functions_registry, \ external_operator_api, _type_to_output_method, _type_to_input_method @@ -46,7 +47,7 @@ def record_operator(operator_type, *args) -> None: operator = operator_type if dpf.SERVER is None: settings.set_server_configuration(server_factory.ServerConfig(None, False)) - server.start_local_server() + server.start_local_server(context=AvailableServerContexts.premium) if len(args) == 2: external_operator_api.external_operator_record_with_abstract_core_and_wrapper( operator._call_back(), diff --git a/ansys/dpf/core/server.py b/ansys/dpf/core/server.py index 53139daf64..e6e71bebb6 100644 --- a/ansys/dpf/core/server.py +++ b/ansys/dpf/core/server.py @@ -18,6 +18,7 @@ from ansys.dpf.core.server_factory import ServerConfig, ServerFactory, CommunicationProtocols from ansys.dpf.core.server_types import DPF_DEFAULT_PORT, LOCALHOST, RUNNING_DOCKER +from ansys.dpf.core import server_context def shutdown_global_server(): @@ -129,7 +130,8 @@ def start_local_server( docker_config=RUNNING_DOCKER, timeout=20., config=None, - use_pypim_by_default=True + use_pypim_by_default=True, + context=None ): """Start a new local DPF server at a given port and IP address. @@ -168,6 +170,10 @@ def start_local_server( use_pypim_by_default: bool, optional Whether to use PyPIM functionalities by default when a PyPIM environment is detected. Defaults to True. + context: ServerContext, optional + Defines the settings that will be used to load DPF's plugins. + A DPF xml file can be used to list the plugins and set up variables. Default is + `server_context.SERVER_CONTEXT`. Returns ------- @@ -207,6 +213,9 @@ def start_local_server( else: docker_config.use_docker = False + if context is None: + context = server_context.SERVER_CONTEXT + server = None n_attempts = 3 timed_out = False @@ -221,11 +230,11 @@ def start_local_server( server = server_type( ansys_path, ip, port, as_global=as_global, launch_server=True, load_operators=load_operators, docker_config=docker_config, timeout=timeout, - use_pypim=use_pypim) + use_pypim=use_pypim, context=context) else: server = server_type( ansys_path, as_global=as_global, - load_operators=load_operators, timeout=timeout) + load_operators=load_operators, timeout=timeout, context=context) break except errors.InvalidPortError: # allow socket in use errors port += 1 @@ -249,7 +258,14 @@ def start_local_server( return server -def connect_to_server(ip=LOCALHOST, port=DPF_DEFAULT_PORT, as_global=True, timeout=5, config=None): +def connect_to_server( + ip=LOCALHOST, + port=DPF_DEFAULT_PORT, + as_global=True, + timeout=5, + config=None, + context=None, +): """Connect to an existing DPF server. This method sets the global default channel that is then used for the @@ -273,6 +289,10 @@ def connect_to_server(ip=LOCALHOST, port=DPF_DEFAULT_PORT, as_global=True, timeo passes, the connection fails. config: ServerConfig, optional Manages the type of server connection to use. + context: ServerContext, optional + Defines the settings that will be used to load DPF's plugins. + A DPF xml file can be used to list the plugins and set up variables. Default is + `server_context.SERVER_CONTEXT`. Examples -------- @@ -293,17 +313,21 @@ def connect_to_server(ip=LOCALHOST, port=DPF_DEFAULT_PORT, as_global=True, timeo >>> #unspecified_server = dpf.connect_to_server(as_global=False) """ + if context is None: + context = server_context.SERVER_CONTEXT def connect(): server_init_signature = inspect.signature(server_type.__init__) if "ip" in server_init_signature.parameters.keys() \ and "port" in server_init_signature.parameters.keys(): server = server_type( - ip=ip, port=port, as_global=as_global, launch_server=False + ip=ip, port=port, as_global=as_global, launch_server=False, + context=context ) else: server = server_type( - as_global=as_global + as_global=as_global, + context=context ) dpf.core._server_instances.append(weakref.ref(server)) return server diff --git a/ansys/dpf/core/server_context.py b/ansys/dpf/core/server_context.py index 5e97196f22..f080b44c61 100644 --- a/ansys/dpf/core/server_context.py +++ b/ansys/dpf/core/server_context.py @@ -14,58 +14,88 @@ from enum import Enum -class EContextType(Enum): - pre_defined_environment = 0 - """DataProcessingCore.xml that is next to DataProcessingCore.dll/libDataProcessingCore.so will - be taken""" +class LicensingContextType(Enum): premium = 1 - """Gets the Specific premium DataProcessingCore.xml.""" - user_defined = 2 - """Load a user defined xml using its path.""" - custom_defined = 3 - """Loads the xml named "DpfCustomDefined.xml" that the user can modify.""" + """Allows capabilities requiring Licenses check out.""" entry = 4 - """Loads the minimum number of plugins for a basic usage.""" + """Loads minimum capabilities without requiring any Licenses check out.""" + + def __int__(self): + return self.value + + @staticmethod + def same_licensing_context(first, second): + if int(first) == int(LicensingContextType.entry) and int(second) != int( + LicensingContextType.entry): + return False + elif int(second) == int(LicensingContextType.entry) and int(first) != int( + LicensingContextType.entry): + return False + return True class ServerContext: """The context allows to choose which capabilities are available server side. + xml_path argument won't be taken into account if using LicensingContextType.entry. Parameters ---------- - context_type : EContextType + context_type : LicensingContextType Type of context. xml_path : str, optional Path to the xml to load. """ - def __init__(self, context_type=EContextType.user_defined, xml_path=""): + + def __init__(self, context_type=LicensingContextType.premium, xml_path=""): self._context_type = context_type self._xml_path = xml_path @property - def context_type(self): + def licensing_context_type(self): + """Whether capabilities requiring Licenses check out should be allowed. + + Returns + ------- + LicensingContextType + """ return self._context_type @property def xml_path(self): + """Path to the xml listing the capabilities to load on the server. + + Returns + ------- + str + """ return self._xml_path def __str__(self): - return f"Server Context of type {self.context_type}" \ - f" with {'no' if len(self.xml_path)==0 else ''} xml path" \ - f"{'' if len(self.xml_path)==0 else ': ' + self.xml_path}" + return f"Server Context of type {self.licensing_context_type}" \ + f" with {'no' if len(self.xml_path) == 0 else ''} xml path" \ + f"{'' if len(self.xml_path) == 0 else ': ' + self.xml_path}" + + def __eq__(self, other): + if not isinstance(other, ServerContext): + return False + return os.path.normpath(self.xml_path) == os.path.normpath(other.xml_path) \ + and LicensingContextType.same_licensing_context(self.licensing_context_type, + other.licensing_context_type) + + def __ne__(self, other): + return not self == other class AvailableServerContexts: - pre_defined_environment = ServerContext(EContextType.pre_defined_environment) + pre_defined_environment = ServerContext(0) """DataProcessingCore.xml that is next to DataProcessingCore.dll/libDataProcessingCore.so will be taken""" - premium = ServerContext(EContextType.premium) + premium = ServerContext(LicensingContextType.premium) """Gets the Specific premium DataProcessingCore.xml to load most plugins with their environments.""" - custom_defined = ServerContext(EContextType.custom_defined) + custom_defined = ServerContext(3) """Loads the xml named "DpfCustomDefined.xml" that the user can modify.""" - entry = ServerContext(EContextType.entry) + entry = ServerContext(LicensingContextType.entry) """Loads the minimum number of plugins for a basic usage. Is the default.""" @@ -80,16 +110,14 @@ class AvailableServerContexts: warnings.warn(UserWarning( f"{DPF_SERVER_CONTEXT_ENV} is set to {default_context}, which is not " f"recognized as an available DPF ServerContext type. \n" - f"Accepted values are: {[t.name.upper() for t in EContextType]}.\n" - f"Using {EContextType.entry.name.upper()} " + f"Accepted values are: {[t.name.upper() for t in LicensingContextType]}.\n" + f"Using {LicensingContextType.entry.name.upper()} " f"as the default ServerContext type.")) -def apply_server_context(context=AvailableServerContexts.entry, server=None) -> None: - """Allows to apply a context globally (if no server is specified) or to a - given server. - When called before any server is started, the context will be applied by default to any - new server. +def set_default_server_context(context=AvailableServerContexts.entry) -> None: + """This context will be applied by default to any new server as well as + the global server, if it's running. The context allows to choose which capabilities are available server side. @@ -98,20 +126,13 @@ def apply_server_context(context=AvailableServerContexts.entry, server=None) -> context : ServerContext Context to apply to the given server or to the newly started servers (when no server is given). - server : server.DPFServer, optional - Server with channel connected to the remote or local instance. When - ``None``, attempts to use the global server. Notes ----- Available with server's version starting at 6.0 (Ansys 2023R2). """ from ansys.dpf.core import SERVER - if server is None: - server = SERVER - global SERVER_CONTEXT - SERVER_CONTEXT = context - if server is not None: - from ansys.dpf.core.core import BaseService - base = BaseService(server, load_operators=False) - base.apply_context(context) + global SERVER_CONTEXT + SERVER_CONTEXT = context + if SERVER is not None: + SERVER.apply_context(context) diff --git a/ansys/dpf/core/server_factory.py b/ansys/dpf/core/server_factory.py index 46f78add4a..a8e092544d 100644 --- a/ansys/dpf/core/server_factory.py +++ b/ansys/dpf/core/server_factory.py @@ -455,6 +455,10 @@ def docker_server_port(self): """ return self._port + @docker_server_port.setter + def docker_server_port(self, val): + self._port = val + @property def server_id(self): """Running Docker Container id. @@ -548,30 +552,42 @@ def remove_docker_image(self): run_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - def init_with_stdout(self, docker_config, LOG, lines, timeout): + def listen_to_process(self, log, cmd_lines, lines, timeout, stdout: bool = True): """Search inside the Docker Container stdout log to fill in this instance's attributes. Parameters ---------- docker_config : DockerConfig - LOG + log Instance of ``logging`` to add debug info to. + cmd_lines: list + Stdout of the shell process run ``docker run`` command. lines : list Internal Container's stdout are copied into ``lines``. timeout : time When to stop searching for stdout. + stdout : bool, optional + Whether to check stdout or stderr. """ - self._docker_config = docker_config - self.server_id = lines[0].replace("\n", "") + self.server_id = cmd_lines[0].replace("\n", "") t_timeout = time.time() + timeout while time.time() < t_timeout: docker_process = subprocess.Popen(f"docker logs {self.server_id}", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=(os.name == 'posix')) self._use_docker = True - for line in io.TextIOWrapper(docker_process.stdout, encoding="utf-8"): - LOG.debug(line) - lines.append(line) + if stdout: + for line in io.TextIOWrapper(docker_process.stdout, encoding="utf-8"): + log.debug(line) + lines.append(line) + else: + for line in io.TextIOWrapper(docker_process.stderr, encoding="utf-8"): + if line not in lines: + # LOG.error(line) + lines.append(line) + + def docker_run_cmd_command(self, docker_server_port, local_port): + return self._docker_config.docker_run_cmd_command(docker_server_port, local_port) def __str__(self): return str(self._docker_config) + f"\t- server_id: {self.server_id}\n" diff --git a/ansys/dpf/core/server_types.py b/ansys/dpf/core/server_types.py index cd0bb77b5f..2238886be9 100644 --- a/ansys/dpf/core/server_types.py +++ b/ansys/dpf/core/server_types.py @@ -12,8 +12,8 @@ import time import warnings import traceback +from threading import Thread, Lock from abc import ABC -from threading import Thread import psutil @@ -95,7 +95,7 @@ def _verify_ansys_path_is_valid(ansys_path, executable, path_in_install=None): def _run_launch_server_process(ip, port, ansys_path=None, - docker_config=server_factory.DockerConfig()): + docker_config=server_factory.RunningDockerConfig()): bShell = False if docker_config.use_docker: docker_server_port = int(os.environ.get("DOCKER_SERVER_PORT", port)) @@ -201,7 +201,8 @@ def launch_dpf(ansys_path, ip=LOCALHOST, port=DPF_DEFAULT_PORT, timeout=10): process, port, timeout, lines, current_errors, stderr=None, stdout=None) -def launch_dpf_on_docker(docker_config, ansys_path=None, ip=LOCALHOST, port=DPF_DEFAULT_PORT, +def launch_dpf_on_docker(running_docker_config, + ansys_path=None, ip=LOCALHOST, port=DPF_DEFAULT_PORT, timeout=10): """Launch Ansys DPF. @@ -223,29 +224,36 @@ def launch_dpf_on_docker(docker_config, ansys_path=None, ip=LOCALHOST, port=DPF_ docker_config : server_factory.DockerConfig, optional To start DPF server as a docker, specify the docker configurations here. - Returns - ------- - running_docker_config : server_factory.RunningDockerConfig - """ - process = _run_launch_server_process(ip, port, ansys_path, docker_config) + process = _run_launch_server_process(ip, port, ansys_path, running_docker_config) # check to see if the service started + cmd_lines = [] + # Creating lock for threads + lock = Lock() + lock.acquire() lines = [] - docker_id = [] current_errors = [] - running_docker_config = server_factory.RunningDockerConfig(docker_server_port=port) + running_docker_config.docker_server_port = port def read_stdout(): for line in io.TextIOWrapper(process.stdout, encoding="utf-8"): LOG.debug(line) - lines.append(line) - running_docker_config.init_with_stdout(docker_config, LOG, lines, timeout) + cmd_lines.append(line) + lock.release() + running_docker_config.listen_to_process(LOG, cmd_lines, lines, timeout) + + def read_stderr(): + for line in io.TextIOWrapper(process.stderr, encoding="utf-8"): + LOG.error(line) + current_errors.append(line) + while lock.locked(): + pass + running_docker_config.listen_to_process(LOG, cmd_lines, current_errors, + timeout, False) _wait_and_check_server_connection( - process, port, timeout, lines, current_errors, stderr=None, stdout=read_stdout) - - return running_docker_config + process, port, timeout, lines, current_errors, stderr=read_stderr, stdout=read_stdout) def launch_remote_dpf(version=None): @@ -342,6 +350,7 @@ def __init__(self): self._server_id = None self._session_instance = None self._base_service_instance = None + self._context = None self._docker_config = server_factory.RunningDockerConfig() def set_as_global(self, as_global=True): @@ -468,6 +477,19 @@ def apply_context(self, context): Available with server's version starting at 6.0 (Ansys 2023R2). """ self._base_service.apply_context(context) + self._context = context + + @property + def context(self): + """Returns the settings used to load DPF's plugins. + To update the context server side, use + :func:`ansys.dpf.core.BaseServer.server_types.apply_context` + + Returns + ------- + ServerContext + """ + return self._context def check_version(self, required_version, msg=None): """Check if the server version matches with a required version. @@ -600,6 +622,7 @@ def __init__(self, docker_config=RUNNING_DOCKER, use_pypim=True, num_connection_tryouts=3, + context=server_context.SERVER_CONTEXT ): # Load DPFClientAPI from ansys.dpf.core.misc import is_pypim_configured @@ -621,8 +644,9 @@ def __init__(self, port = int(address.split(":")[-1]) elif docker_config.use_docker: - self.docker_config = launch_dpf_on_docker( - docker_config=docker_config, + self.docker_config = server_factory.RunningDockerConfig(docker_config) + launch_dpf_on_docker( + running_docker_config=self.docker_config, ansys_path=ansys_path, ip=ip, port=port, timeout=timeout) else: launch_dpf(ansys_path, ip, port, timeout=timeout) @@ -637,11 +661,12 @@ def __init__(self, self.live = True self._create_shutdown_funcs() self._check_first_call(num_connection_tryouts) - self.set_as_global(as_global=as_global) try: - self._base_service.initialize_with_context(server_context.SERVER_CONTEXT) + self._base_service.initialize_with_context(context) + self._context = context except errors.DpfVersionNotSupported: pass + self.set_as_global(as_global=as_global) def _check_first_call(self, num_connection_tryouts): for i in range(num_connection_tryouts): @@ -679,11 +704,13 @@ def shutdown(self): if self._remote_instance: self._remote_instance.delete() try: - self._preparing_shutdown_func[0](self._preparing_shutdown_func[1]) + if hasattr(self, "_preparing_shutdown_func"): + self._preparing_shutdown_func[0](self._preparing_shutdown_func[1]) except Exception as e: warnings.warn("couldn't prepare shutdown: " + str(e.args)) try: - self._shutdown_func[0](self._shutdown_func[1]) + if hasattr(self, "_shutdown_func"): + self._shutdown_func[0](self._shutdown_func[1]) except Exception as e: warnings.warn("couldn't shutdown server: " + str(e.args)) self._docker_config.remove_docker_image() @@ -787,7 +814,9 @@ def __init__(self, ansys_path=None, as_global=True, load_operators=True, - timeout=None): + timeout=None, + context=server_context.SERVER_CONTEXT + ): # Load DPFClientAPI super().__init__(ansys_path=ansys_path, load_operators=load_operators) # Load DataProcessingCore @@ -802,14 +831,15 @@ def __init__(self, f"DPF directory not found at {os.path.dirname(path)}" f"Unable to locate the following file: {path}") raise e - self.set_as_global(as_global=as_global) try: - self._base_service.apply_context(server_context.SERVER_CONTEXT) + self.apply_context(context) except errors.DpfVersionNotSupported: self._base_service.initialize_with_context( server_context.AvailableServerContexts.premium ) + self._context = server_context.AvailableServerContexts.premium pass + self.set_as_global(as_global=as_global) @property def version(self): @@ -896,6 +926,7 @@ def __init__( launch_server=True, docker_config=RUNNING_DOCKER, use_pypim=True, + context=server_context.SERVER_CONTEXT ): """Start the DPF server.""" # Use ansys.grpc.dpf @@ -928,9 +959,11 @@ def __init__( else: if docker_config.use_docker: - self.docker_config = launch_dpf_on_docker(docker_config=docker_config, - ansys_path=ansys_path, ip=ip, - port=port, timeout=timeout) + self.docker_config = server_factory.RunningDockerConfig(docker_config) + launch_dpf_on_docker( + running_docker_config=self.docker_config, + ansys_path=ansys_path, ip=ip, + port=port, timeout=timeout) else: launch_dpf(ansys_path, ip, port, timeout=timeout) self._local_server = True @@ -948,11 +981,12 @@ def __init__( self._create_shutdown_funcs() check_ansys_grpc_dpf_version(self, timeout) - self.set_as_global(as_global=as_global) try: - self._base_service.initialize_with_context(server_context.SERVER_CONTEXT) + self._base_service.initialize_with_context(context) + self._context = context except errors.DpfVersionNotSupported: pass + self.set_as_global(as_global=as_global) def _create_shutdown_funcs(self): self._core_api = data_processing_grpcapi.DataProcessingGRPCAPI @@ -1076,7 +1110,8 @@ def local_server(self, val): def shutdown(self): if self._own_process and self.live: try: - self._preparing_shutdown_func[0](self._preparing_shutdown_func[1]) + if hasattr(self, "_preparing_shutdown_func"): + self._preparing_shutdown_func[0](self._preparing_shutdown_func[1]) except Exception as e: warnings.warn("couldn't prepare shutdown: " + str(e.args)) @@ -1084,7 +1119,8 @@ def shutdown(self): self._remote_instance.delete() else: try: - self._shutdown_func[0](self._shutdown_func[1]) + if hasattr(self, "_shutdown_func"): + self._shutdown_func[0](self._shutdown_func[1]) except Exception as e: try: if self.meet_version("4.0"): diff --git a/ansys/dpf/core/settings.py b/ansys/dpf/core/settings.py index 58d34d51da..14de522e33 100644 --- a/ansys/dpf/core/settings.py +++ b/ansys/dpf/core/settings.py @@ -8,6 +8,7 @@ from ansys.dpf.core.misc import module_exists from ansys.dpf.core import misc from ansys.dpf.core.server import set_server_configuration # noqa: F401 +from ansys.dpf.core.server_context import set_default_server_context # noqa: F401 from ansys.dpf.core.server_factory import ServerConfig # noqa: F401 from ansys.dpf.core import core diff --git a/tests/conftest.py b/tests/conftest.py index 4ed8e8d742..6e43b6af0e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -446,6 +446,6 @@ def count_servers(): if SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0: core.server.shutdown_all_session_servers() try: - core.apply_server_context(core.AvailableServerContexts.premium) + core.set_default_server_context(core.AvailableServerContexts.premium) except core.errors.DpfVersionNotSupported: pass diff --git a/tests/test_multi_server.py b/tests/test_multi_server.py index 596b1fe901..a744b69bfb 100644 --- a/tests/test_multi_server.py +++ b/tests/test_multi_server.py @@ -8,7 +8,7 @@ from ansys.dpf.core.server_factory import ServerConfig, CommunicationProtocols if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0: - dpf.apply_server_context(dpf.AvailableServerContexts.entry) + dpf.set_default_server_context(dpf.AvailableServerContexts.entry) @pytest.fixture(scope="module", params=[ @@ -32,7 +32,7 @@ def other_remote_server(request): if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0: dpf.server.shutdown_all_session_servers() - dpf.apply_server_context(dpf.AvailableServerContexts.premium) + dpf.set_default_server_context(dpf.AvailableServerContexts.premium) @pytest.fixture() diff --git a/tests/test_remote_workflow.py b/tests/test_remote_workflow.py index 875dbf1db1..8f5ae8757b 100644 --- a/tests/test_remote_workflow.py +++ b/tests/test_remote_workflow.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pytest @@ -506,9 +508,11 @@ def test_multi_process_transparent_api_connect_local_op_remote_workflow(): @pytest.mark.xfail(raises=ServerTypeError) -@pytest.mark.skipif(not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_3_0, - reason='Connecting data from different servers is ' - 'supported starting server version 3.0') +@pytest.mark.skipif( + (not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_4_0 and os.name == "posix") and + not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_3_0, + reason='Connecting data from different servers is ' + 'supported starting server version 3.0') def test_multi_process_transparent_api_create_on_local_remote_workflow(): files = examples.download_distributed_files() wf = core.Workflow() diff --git a/tests/test_server.py b/tests/test_server.py index 851e758ef5..9e62ed93a1 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -146,8 +146,11 @@ def test_docker_busy_port(remote_config_server_type, clean_up): my_serv = start_local_server(config=remote_config_server_type) busy_port = my_serv.external_port with pytest.raises(errors.InvalidPortError): + running_docker_config = dpf.core.server_factory.RunningDockerConfig( + docker_config=dpf.core.server.RUNNING_DOCKER + ) server_types.launch_dpf_on_docker(port=busy_port, - docker_config=dpf.core.server.RUNNING_DOCKER + running_docker_config=running_docker_config ) server = start_local_server(as_global=False, port=busy_port, config=remote_config_server_type) diff --git a/tests/test_service.py b/tests/test_service.py index e47d90368e..b41553a473 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -5,6 +5,7 @@ import pkgutil import datetime import platform +from importlib import reload from ansys import dpf from ansys.dpf.core import path_utilities @@ -445,16 +446,19 @@ def set_context_back_to_premium(request): """Count servers once we are finished.""" dpf.core.server.shutdown_all_session_servers() + init_val = os.environ.get("ANSYS_DPF_ACCEPT_LA", None) try: - dpf.core.apply_server_context(dpf.core.AvailableServerContexts.entry) + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.entry) except dpf.core.errors.DpfVersionNotSupported: pass def revert(): + if init_val: + os.environ["ANSYS_DPF_ACCEPT_LA"] = init_val dpf.core.SERVER_CONFIGURATION = None dpf.core.server.shutdown_all_session_servers() try: - dpf.core.apply_server_context(dpf.core.AvailableServerContexts.premium) + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium) except dpf.core.errors.DpfVersionNotSupported: pass @@ -476,6 +480,13 @@ def reset_context_environment_variable(request): def revert(): if init_context: os.environ[key] = init_context + else: + del os.environ[key] + reload(s_c) + try: + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium) + except dpf.core.errors.DpfVersionNotSupported: + pass request.addfinalizer(revert) @@ -484,7 +495,6 @@ def revert(): reason="AWP ROOT is not set with Docker") @conftest.raises_for_servers_version_under("6.0") def test_context_environment_variable(reset_context_environment_variable): - from importlib import reload from ansys.dpf.core import server_context as s_c key = s_c.DPF_SERVER_CONTEXT_ENV @@ -497,7 +507,7 @@ def test_context_environment_variable(reset_context_environment_variable): assert s_c.SERVER_CONTEXT == s_c.AvailableServerContexts.entry # Test each possible value is correctly understood and sets SERVER_CONTEXT - for context in s_c.EContextType: + for context in s_c.LicensingContextType: os.environ[key] = context.name.upper() reload(s_c) try: @@ -507,6 +517,41 @@ def test_context_environment_variable(reset_context_environment_variable): @pytest.mark.order(1) +@pytest.mark.skipif(running_docker or os.environ.get("ANSYS_DPF_ACCEPT_LA", None) is None, + reason="Tests ANSYS_DPF_ACCEPT_LA") +@conftest.raises_for_servers_version_under("6.0") +def test_license_agr(set_context_back_to_premium): + config = dpf.core.AvailableServerConfigs.InProcessServer + init_val = os.environ["ANSYS_DPF_ACCEPT_LA"] + del os.environ["ANSYS_DPF_ACCEPT_LA"] + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.start_local_server(config=config, as_global=True) + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.Operator("stream_provider") + os.environ["ANSYS_DPF_ACCEPT_LA"] = init_val + dpf.core.start_local_server(config=config, as_global=True) + assert "static" in examples.find_static_rst() + assert dpf.core.Operator("stream_provider") is not None + + +@pytest.mark.order(2) +@pytest.mark.skipif(os.environ.get("ANSYS_DPF_ACCEPT_LA", None) is None, + reason="Tests ANSYS_DPF_ACCEPT_LA") +@conftest.raises_for_servers_version_under("6.0") +def test_license_agr_remote(remote_config_server_type, set_context_back_to_premium): + init_val = os.environ["ANSYS_DPF_ACCEPT_LA"] + del os.environ["ANSYS_DPF_ACCEPT_LA"] + with pytest.raises(RuntimeError): # runtime error raised when server is started + dpf.core.start_local_server(config=remote_config_server_type, as_global=True) + with pytest.raises((dpf.core.errors.DPFServerException, RuntimeError)): + dpf.core.Operator("stream_provider") + os.environ["ANSYS_DPF_ACCEPT_LA"] = init_val + dpf.core.start_local_server(config=remote_config_server_type, as_global=True) + assert "static" in examples.find_static_rst() + assert dpf.core.Operator("stream_provider") is not None + + +@pytest.mark.order(3) @pytest.mark.skipif(running_docker or not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_5_0, reason="AWP ROOT is not set with Docker") @conftest.raises_for_servers_version_under("6.0") @@ -519,14 +564,26 @@ def test_apply_context(set_context_back_to_premium): if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0: with pytest.raises(KeyError): dpf.core.Operator("core::field::high_pass") + with pytest.raises(dpf.core.errors.DPFServerException): + if dpf.core.SERVER.os == "nt": + dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators") + else: + dpf.core.load_library("libAns.Dpf.Math.so", "math_operators") + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.entry else: dpf.core.start_local_server() - dpf.core.apply_server_context(dpf.core.AvailableServerContexts.premium, dpf.core.SERVER) + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium) + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium dpf.core.Operator("core::field::high_pass") + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.entry) + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.SERVER.apply_context(dpf.core.AvailableServerContexts.entry) + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium -@pytest.mark.order(2) +@pytest.mark.order(4) @pytest.mark.skipif(not conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_5_0, reason="not supported") @conftest.raises_for_servers_version_under("6.0") @@ -536,17 +593,31 @@ def test_apply_context_remote(remote_config_server_type, set_context_back_to_pre if conftest.SERVERS_VERSION_GREATER_THAN_OR_EQUAL_TO_6_0: with pytest.raises(dpf.core.errors.DPFServerException): dpf.core.Operator("core::field::high_pass") + with pytest.raises(dpf.core.errors.DPFServerException): + if dpf.core.SERVER.os == "nt": + dpf.core.load_library("Ans.Dpf.Math.dll", "math_operators") + else: + dpf.core.load_library("libAns.Dpf.Math.so", "math_operators") + + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.entry else: dpf.core.start_local_server() - dpf.core.apply_server_context(dpf.core.AvailableServerContexts.premium, dpf.core.SERVER) + dpf.core.SERVER.apply_context(dpf.core.AvailableServerContexts.premium) dpf.core.Operator("core::field::high_pass") + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium dpf.core.server.shutdown_all_session_servers() with pytest.raises(dpf.core.errors.DPFServerException): dpf.core.Operator("core::field::high_pass") - dpf.core.apply_server_context(dpf.core.AvailableServerContexts.premium) + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.premium) dpf.core.Operator("core::field::high_pass") + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.set_default_server_context(dpf.core.AvailableServerContexts.entry) + with pytest.raises(dpf.core.errors.DPFServerException): + dpf.core.SERVER.apply_context(dpf.core.AvailableServerContexts.entry) + + assert dpf.core.SERVER.context == dpf.core.AvailableServerContexts.premium @pytest.mark.order("last")