diff --git a/.ci/build_matrix.sh b/.ci/build_matrix.sh index 7c091ceaeb..04d89e2ef3 100755 --- a/.ci/build_matrix.sh +++ b/.ci/build_matrix.sh @@ -1,5 +1,9 @@ #!/bin/bash +# **** REMEMBER ***** +# Remember to update the env var ``LATEST_VERSION`` in ci.yml +# + # List of versions versions=( # if added more "latest", change "$LATEST" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 04e1910ac9..a8a6b52c7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,13 +24,14 @@ env: PACKAGE_NAME: 'ansys-mapdl-core' PACKAGE_NAMESPACE: 'ansys.mapdl.core' DOCUMENTATION_CNAME: 'mapdl.docs.pyansys.com' + LATEST_VERSION: "242" + MAPDL_IMAGE_VERSION_DOCS_BUILD: v24.2-ubuntu-student MEILISEARCH_API_KEY: ${{ secrets.MEILISEARCH_API_KEY }} MEILISEARCH_PUBLIC_API_KEY: ${{ secrets.MEILISEARCH_PUBLIC_API_KEY }} PYANSYS_OFF_SCREEN: True DPF_START_SERVER: False DPF_PORT: 21004 MAPDL_PACKAGE: ghcr.io/ansys/mapdl - MAPDL_IMAGE_VERSION_DOCS_BUILD: v24.1-ubuntu-student ON_CI: True PYTEST_ARGUMENTS: '-vvv -ra --durations=10 --maxfail=3 --reruns 3 --reruns-delay 4 --cov=ansys.mapdl.core --cov-report=html' @@ -807,7 +808,7 @@ jobs: # executable path with the env var: PYMAPDL_MAPDL_EXEC. if [[ "${{ matrix.mapdl-version }}" == *"latest-ubuntu"* ]] ; then - version="242" + version=${{ env.LATEST_VERSION }} else version=$(echo "${{ matrix.mapdl-version }}" | head -c 5 | tail -c 4 | tr -d '.') fi; diff --git a/doc/changelog.d/3226.fixed.md b/doc/changelog.d/3226.fixed.md new file mode 100644 index 0000000000..16cbfa38a7 --- /dev/null +++ b/doc/changelog.d/3226.fixed.md @@ -0,0 +1 @@ +fix: missing arguments on ``OCDATA`` command \ No newline at end of file diff --git a/doc/changelog.d/3261.changed.md b/doc/changelog.d/3261.changed.md new file mode 100644 index 0000000000..dcfbe22428 --- /dev/null +++ b/doc/changelog.d/3261.changed.md @@ -0,0 +1 @@ +build: bump zipp from 3.17.0 to 3.19.1 in /doc/source/examples/extended_examples/hpc \ No newline at end of file diff --git a/doc/changelog.d/3266.fixed.md b/doc/changelog.d/3266.fixed.md new file mode 100644 index 0000000000..0d868fb650 --- /dev/null +++ b/doc/changelog.d/3266.fixed.md @@ -0,0 +1 @@ +fix: pool issues \ No newline at end of file diff --git a/doc/source/examples/extended_examples/hpc/requirements.txt b/doc/source/examples/extended_examples/hpc/requirements.txt index 92bd8b4f11..15f2802e9a 100644 --- a/doc/source/examples/extended_examples/hpc/requirements.txt +++ b/doc/source/examples/extended_examples/hpc/requirements.txt @@ -40,4 +40,4 @@ tabulate==0.9.0 tqdm==4.66.3 urllib3==2.2.2 vtk==9.3.0 -zipp==3.17.0 +zipp==3.19.1 diff --git a/src/ansys/mapdl/core/_commands/solution/ocean.py b/src/ansys/mapdl/core/_commands/solution/ocean.py index ba277932a1..94aeb88434 100644 --- a/src/ansys/mapdl/core/_commands/solution/ocean.py +++ b/src/ansys/mapdl/core/_commands/solution/ocean.py @@ -22,10 +22,27 @@ class Ocean: - def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): + def ocdata( + self, + val1: str = "", + val2: str = "", + val3: str = "", + val4: str = "", + val5: str = "", + val6: str = "", + val7: str = "", + val8: str = "", + val9: str = "", + val10: str = "", + val11: str = "", + val12: str = "", + val13: str = "", + val14: str = "", + **kwargs, + ): """Defines an ocean load using non-table data. - APDL Command: OCDATA + APDL Command: ``OCDATA`` Parameters ---------- @@ -34,14 +51,14 @@ def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): Notes ----- - The OCDATA command specifies non-table data that defines the ocean + The ``OCDATA`` command specifies non-table data that defines the ocean load, such as the depth of the ocean to the mud line, the ratio of added mass over added mass for a circular cross section, or the wave type to apply. The terms VAL1, VAL2, etc. are specialized according to the input set required for the given ocean load. - The program interprets the data input via the OCDATA command within the - context of the most recently issued OCTYPE command. + The program interprets the data input via the ``OCDATA`` command within the + context of the most recently issued ``OCTYPE`` command. Input values in the order indicated. @@ -52,19 +69,19 @@ def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): For a better understanding of how to set up a basic ocean type, see Figure: 5:: Basic Ocean Data Type Components . - DEPTH -- The depth of the ocean (that is, the distance between the mean + ``DEPTH`` -- The depth of the ocean (that is, the distance between the mean sea level and the mud line). The water surface is assumed to be level in the XY plane, with Z being positive upwards. This value is required and must be positive. - MATOC -- The material number of the ocean. This value is required and + ``MATOC`` -- The material number of the ocean. This value is required and is used to input the required density. It is also used to input the - viscosity if the Reynolds number is used (OCTABLE). + viscosity if the Reynolds number is used (``OCTABLE``). - KFLOOD -- The inside-outside fluid-interaction key: + ``KFLOOD`` -- The inside-outside fluid-interaction key: - For beam subtype CTUBE and HREC used with BEAM188 or BEAM189 and ocean - loading, KFLOOD is always set to 1. + For beam subtype ``CTUBE`` and ``HREC`` used with BEAM188 or BEAM189 and ocean + loading, ``KFLOOD`` is always set to 1. Cay -- The ratio of added mass of the external fluid over the mass of the fluid displaced by the element cross section in the y direction @@ -73,7 +90,7 @@ def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): element moves in the element y direction during a dynamic analysis. If no value is specified, and the coefficient of inertia CMy is not - specified (OCTABLE), both values default to 0.0. + specified (``OCTABLE``), both values default to 0.0. If no value is specified, but CMy is specified, this value defaults to Cay = CMy - 1.0. @@ -90,7 +107,7 @@ def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): Cay. If no value is specified, and the coefficient of inertia CMz is not - specified (OCTABLE), both values default to 0.0. + specified (``OCTABLE``), both values default to 0.0. If no value is specified, but CMz is specified, this value defaults to Cay = CMz - 1.0. @@ -115,31 +132,31 @@ def ocdata(self, val1="", val2="", val3="", val14="", **kwargs): Two example cases for Zmsl are: - A structure with its origin on the sea floor (Zmsl = DEPTH). + A structure with its origin on the sea floor (Zmsl = ``DEPTH``). - A tidal change (tc) above the mean sea level (Zmsl = tc, and DEPTH - becomes DEPTH + tc) + A tidal change (tc) above the mean sea level (Zmsl = tc, and ``DEPTH`` + becomes ``DEPTH`` + tc) - Ktable -- The dependency of VAL1 on the OCTABLE command: + Ktable -- The dependency of VAL1 on the ``OCTABLE`` command: - KWAVE -- The incident wave type: + ``KWAVE`` -- The incident wave type: - THETA -- Angle of the wave direction θ from the global Cartesian X axis + ``THETA`` -- Angle of the wave direction θ from the global Cartesian X axis toward the global Cartesian Y axis (in degrees). - WAVELOC (valid when KWAVE = 0 through 3, and 101+) -- The wave location + ``WAVELOC`` (valid when ``KWAVE`` = 0 through 3, and 101+) -- The wave location type: - SPECTRUM (valid when KWAVE = 5 through 7) -- The wave spectrum type: + ``SPECTRUM`` (valid when ``KWAVE`` = 5 through 7) -- The wave spectrum type: - KCRC -- The wave-current interaction key. + ``KCRC`` -- The wave-current interaction key. - Adjustments to the current profile are available via the KCRC constant + Adjustments to the current profile are available via the ``KCRC`` constant of the water motion table. Typically, these options are used only when the wave amplitude is large relative to the water depth, such that significant wave-current interaction exists. """ - command = f"OCDATA,{val1},{val2},{val3},{val14}" + command = f"OCDATA,{val1},{val2},{val3},{val4},{val5},{val6},{val7},{val8},{val9},{val10},{val11},{val12},{val13},{val14}" return self.run(command, **kwargs) def ocdelete(self, datatype="", zonename="", **kwargs): diff --git a/src/ansys/mapdl/core/cli/list_instances.py b/src/ansys/mapdl/core/cli/list_instances.py index 98fa87dbf1..11a3dba169 100644 --- a/src/ansys/mapdl/core/cli/list_instances.py +++ b/src/ansys/mapdl/core/cli/list_instances.py @@ -73,23 +73,35 @@ def list_instances(instances, long, cmd, location): mapdl_instances = [] def is_valid_process(proc): - valid_status = proc.status in [psutil.STATUS_RUNNING, psutil.STATUS_IDLE] + valid_status = proc.status() in [ + psutil.STATUS_RUNNING, + psutil.STATUS_IDLE, + psutil.STATUS_SLEEPING, + ] valid_ansys_process = ("ansys" in proc.name().lower()) or ( "mapdl" in proc.name().lower() ) + # Early exit to avoid checking 'cmdline' of a protected process (raises psutil.AccessDenied) + if not valid_ansys_process: + return False + grpc_is_active = "-grpc" in proc.cmdline() return valid_status and valid_ansys_process and grpc_is_active for proc in psutil.process_iter(): # Check if the process is running and not suspended - if is_valid_process(proc): - # Checking the number of children we infer if the process is the main process, - # or one of the main process thread. - if len(proc.children(recursive=True)) < 2: - proc.ansys_instance = False - else: - proc.ansys_instance = True - mapdl_instances.append(proc) + try: + if is_valid_process(proc): + # Checking the number of children we infer if the process is the main process, + # or one of the main process thread. + if len(proc.children(recursive=True)) < 2: + proc.ansys_instance = False + else: + proc.ansys_instance = True + mapdl_instances.append(proc) + + except (psutil.NoSuchProcess, psutil.ZombieProcess) as e: + continue # printing table = [] diff --git a/src/ansys/mapdl/core/cli/stop.py b/src/ansys/mapdl/core/cli/stop.py index be82be670d..582d3c3747 100644 --- a/src/ansys/mapdl/core/cli/stop.py +++ b/src/ansys/mapdl/core/cli/stop.py @@ -23,12 +23,6 @@ import click -def is_ansys_process(proc): - return ( - "ansys" in proc.name().lower() or "mapdl" in proc.name().lower() - ) and "-grpc" in proc.cmdline() - - @click.command( short_help="Stop MAPDL instances.", help="""This command stop MAPDL instances running on a given port or with a given process id (PID). @@ -58,6 +52,8 @@ def is_ansys_process(proc): def stop(port, pid, all): import psutil + from ansys.mapdl.core.launcher import is_ansys_process + PROCESS_OK_STATUS = [ # List of all process status, comment out the ones that means that # process is not OK. @@ -84,28 +80,32 @@ def stop(port, pid, all): if port or all: killed_ = False for proc in psutil.process_iter(): - if ( - psutil.pid_exists(proc.pid) - and proc.status() in PROCESS_OK_STATUS - and is_ansys_process(proc) - ): - # Killing "all" - if all: - try: - proc.kill() - killed_ = True - except psutil.NoSuchProcess: - pass - - else: - # Killing by ports - if str(port) in proc.cmdline(): + try: + if ( + psutil.pid_exists(proc.pid) + and proc.status() in PROCESS_OK_STATUS + and is_ansys_process(proc) + ): + # Killing "all" + if all: try: proc.kill() killed_ = True except psutil.NoSuchProcess: pass + else: + # Killing by ports + if str(port) in proc.cmdline(): + try: + proc.kill() + killed_ = True + except psutil.NoSuchProcess: + pass + + except psutil.NoSuchProcess: + continue + if all: str_ = "" else: diff --git a/src/ansys/mapdl/core/launcher.py b/src/ansys/mapdl/core/launcher.py index f359f4afb1..a33f7e9a5d 100644 --- a/src/ansys/mapdl/core/launcher.py +++ b/src/ansys/mapdl/core/launcher.py @@ -256,6 +256,9 @@ def get_process_at_port(port) -> Optional[psutil.Process]: ) # just to check if we can access the except psutil.AccessDenied: continue + except psutil.NoSuchProcess: + # process already died + continue for conns in connections: if conns.laddr.port == port: diff --git a/src/ansys/mapdl/core/mapdl_grpc.py b/src/ansys/mapdl/core/mapdl_grpc.py index 436dd594dd..32f60d1a13 100644 --- a/src/ansys/mapdl/core/mapdl_grpc.py +++ b/src/ansys/mapdl/core/mapdl_grpc.py @@ -1059,9 +1059,12 @@ def _remove_temp_dir_on_exit(self, path=None): """ if self.remove_temp_dir_on_exit and self._local: - path = path or self.directory + from pathlib import Path + + path = str(Path(path or self.directory)) tmp_dir = tempfile.gettempdir() - ans_temp_dir = os.path.join(tmp_dir, "ansys_") + ans_temp_dir = str(Path(os.path.join(tmp_dir, "ansys_"))) + if path.startswith(ans_temp_dir): self._log.debug("Removing the MAPDL temporary directory %s", path) shutil.rmtree(path, ignore_errors=True) diff --git a/src/ansys/mapdl/core/pool.py b/src/ansys/mapdl/core/pool.py index 707daaaa25..feff0540c0 100755 --- a/src/ansys/mapdl/core/pool.py +++ b/src/ansys/mapdl/core/pool.py @@ -30,7 +30,7 @@ import weakref from ansys.mapdl.core import LOG, launch_mapdl -from ansys.mapdl.core.errors import MapdlRuntimeError, VersionError +from ansys.mapdl.core.errors import MapdlDidNotStart, MapdlRuntimeError, VersionError from ansys.mapdl.core.launcher import ( LOCALHOST, MAPDL_DEFAULT_PORT, @@ -59,6 +59,7 @@ def available_ports(n_ports: int, starting_port: int = MAPDL_DEFAULT_PORT) -> List[int]: """Return a list the first ``n_ports`` ports starting from ``starting_port``.""" + LOG.debug(f"Getting {n_ports} available ports starting from {starting_port}.") port = starting_port ports: List[int] = [] while port < 65536 and len(ports) < n_ports: @@ -71,6 +72,7 @@ def available_ports(n_ports: int, starting_port: int = MAPDL_DEFAULT_PORT) -> Li f"There are not {n_ports} available ports between {starting_port} and 65536" ) + LOG.debug(f"Retrieved the following available ports: {ports}") return ports @@ -209,10 +211,12 @@ def __init__( override=True, start_instance: bool = None, exec_file: Optional[str] = None, + timeout: int = 30, **kwargs, ) -> None: """Initialize several instances of mapdl""" self._instances: List[None] = [] + self._n_instances = n_instances # Getting debug arguments _debug_no_launch = kwargs.pop("_debug_no_launch", None) @@ -256,11 +260,9 @@ def __init__( n_instances, ip, port ) - # Converting ip or hostname to ip - ips = [socket.gethostbyname(each) for each in ips] - _ = [check_valid_ip(each) for each in ips] # double check - self._ips = ips + LOG.debug(f"Using ports: {ports}") + LOG.debug(f"Using IPs: {ips}") if not names: names = "Instance" @@ -303,7 +305,6 @@ def __init__( self._exec_file = exec_file - # grab available ports if ( start_instance and self._root_dir is not None @@ -311,8 +312,6 @@ def __init__( ): os.makedirs(self._root_dir) - LOG.debug(f"Using ports: {ports}") - self._instances = [] self._active = True # used by pool monitor @@ -341,6 +340,10 @@ def __init__( } return + # Converting ip or hostname to ip + self._ips = [socket.gethostbyname(each) for each in self._ips] + _ = [check_valid_ip(each) for each in self._ips] # double check + threads = [ self._spawn_mapdl( i, @@ -357,13 +360,21 @@ def __init__( if wait: [thread.join() for thread in threads] - # check if all clients connected have connected - if len(self) != n_instances: - n_connected = len(self) - warnings.warn( - f"Only %d clients connected out of %d requested" - % (n_connected, n_instances) + # make sure everything is ready + timeout = time.time() + timeout + + while timeout > time.time(): + n_instances_ready = sum([each is not None for each in self._instances]) + + if n_instances_ready == n_instances: + # Loaded + break + time.sleep(0.1) + else: + raise TimeoutError( + f"Only {n_instances_ready} of {n_instances} could be started." ) + if pbar is not None: pbar.close() @@ -392,6 +403,26 @@ def _exiting(self) -> bool: return self._exiting_i != 0 + @property + def ready(self) -> bool: + """Return true if all the instances are ready (not exited)""" + return ( + sum([each is not None and not each._exited for each in self._instances]) + == self._n_instances + ) + + def wait_for_ready(self, timeout: Optional[int] = 180) -> bool: + """Wait until pool is ready.""" + timeout_ = time.time() + timeout + while time.time() < timeout_: + if self.ready: + break + time.sleep(0.1) + else: + raise TimeoutError( + f"MapdlPool is not ready after waiting {timeout} seconds." + ) + def _verify_unique_ports(self) -> None: if len(self._ports) != len(self): raise MapdlRuntimeError("MAPDLPool has overlapping ports") @@ -481,10 +512,10 @@ def map( results = [] - if iterable is not None: - n = len(iterable) - else: + if iterable is None: n = len(self) + else: + n = len(iterable) pbar = None if progress_bar: @@ -496,11 +527,13 @@ def map( pbar = tqdm(total=n, desc="MAPDL Running") + # monitor thread @threaded_daemon def func_wrapper(obj, func, timeout, args=None): """Expect obj to be an instance of Mapdl""" complete = [False] + # execution thread. @threaded_daemon def run(): if args is not None: @@ -550,7 +583,17 @@ def run(): pbar.update(1) threads = [] - if iterable is not None: + if iterable is None: + # simply apply to all + for instance in self._instances: + if instance: + threads.append(func_wrapper(instance, func, timeout)) + + # wait for all threads to complete + if wait: + [thread.join() for thread in threads] + + else: threads = [] for args in iterable: # grab the next available instance of mapdl @@ -581,15 +624,6 @@ def run(): if wait: [thread.join() for thread in threads] - else: # simply apply to all - for instance in self._instances: - if instance: - threads.append(func_wrapper(instance, func, timeout)) - - # wait for all threads to complete - if wait: - [thread.join() for thread in threads] - return results def run_batch( @@ -860,12 +894,15 @@ def _spawn_mapdl( name: str = "", start_instance=True, exec_file=None, + timeout: int = 30, + run_location: Optional[str] = None, ): """Spawn a mapdl instance at an index""" # create a new temporary directory for each instance self._spawning_i += 1 - run_location = create_temp_dir(self._root_dir, name=name) + if not run_location: + run_location = create_temp_dir(self._root_dir, name=name) self._instances[index] = launch_mapdl( exec_file=exec_file, @@ -880,11 +917,26 @@ def _spawn_mapdl( # Waiting for the instance being fully initialized. # This is introduce to mitigate #2173 - while self._instances[index] is None: + timeout = time.time() + timeout + + def initialized(index): + if self._instances[index] is not None: + if self._instances[index].exited: + raise MapdlRuntimeError("The instance is already exited!") + if "PREP" not in self._instances[index].prep7().upper(): + raise MapdlDidNotStart("Error while processing PREP7 signal.") + return True + return False + + while timeout > time.time(): + if initialized(index): + break time.sleep(0.1) - - assert not self._instances[index].exited - self._instances[index].prep7() + else: + if not initialized: + raise TimeoutError( + f"The instance running at {ip}:{port} could not be started." + ) # LOG.debug("Spawned instance %d. Name '%s'", index, name) if pbar is not None: @@ -896,8 +948,6 @@ def _spawn_mapdl( def _monitor_pool(self, refresh=1.0): """Checks if instances within a pool have exited (failed) and restarts them. - - """ while self._active: for index, instance in enumerate(self._instances): @@ -916,6 +966,7 @@ def _monitor_pool(self, refresh=1.0): thread_name=name, exec_file=self._exec_file, start_instance=self._start_instance, + run_location=instance._path, ).join() except Exception as e: @@ -932,6 +983,7 @@ def __repr__(self): return "MAPDL Pool with %d active instances" % len(self) def _set_n_instance_ip_port_args(self, n_instances, ip, port): + LOG.debug(f"Input n_instances ({n_instances}), ip ({ip}), and port ({port})") if n_instances is None: if ip is None or (isinstance(ip, list) and len(ip) == 0): if port is None or (isinstance(port, list) and len(port) < 1): diff --git a/tests/conftest.py b/tests/conftest.py index 646f08e201..6702346cba 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,9 +24,12 @@ import os from pathlib import Path from shutil import get_terminal_size +import subprocess from sys import platform +import time from _pytest.terminal import TerminalReporter # for terminal customization +import psutil import pytest from common import ( @@ -69,6 +72,7 @@ IS_SMP = is_smp() QUICK_LAUNCH_SWITCHES = "-smp -m 100 -db 100" +VALID_PORTS = [] ## Skip ifs skip_on_windows = pytest.mark.skipif(ON_WINDOWS, reason="Skip on Windows") @@ -460,6 +464,46 @@ def run_before_and_after_tests_2(request, mapdl): assert prev == mapdl.is_local +@pytest.fixture(autouse=True, scope="function") +def run_before_and_after_tests_3(request, mapdl): + """Make sure we leave no MAPDL running behind""" + from ansys.mapdl.core.launcher import is_ansys_process + + PROCESS_OK_STATUS = [ + psutil.STATUS_RUNNING, # + psutil.STATUS_SLEEPING, # + psutil.STATUS_DISK_SLEEP, + psutil.STATUS_DEAD, + psutil.STATUS_PARKED, # (Linux) + psutil.STATUS_IDLE, # (Linux, macOS, FreeBSD) + ] + + yield + + if ON_LOCAL: + for proc in psutil.process_iter(): + try: + if ( + psutil.pid_exists(proc.pid) + and proc.status() in PROCESS_OK_STATUS + and is_ansys_process(proc) + ): + + cmdline = proc.cmdline() + port = int(cmdline[cmdline.index("-port") + 1]) + + if port not in VALID_PORTS: + cmdline_ = " ".join([f'"{each}"' for each in cmdline]) + subprocess.run(["pymapdl", "stop", "--port", f"{port}"]) + time.sleep(1) + # raise Exception( + # f"The following MAPDL instance running at port {port} is alive after the test.\n" + # f"Only ports {VALID_PORTS} are allowed.\nCMD: {cmdline_}" + # ) + except psutil.NoSuchProcess: + continue + + @pytest.fixture(scope="session") def mapdl_console(request): if os.name != "posix": @@ -517,6 +561,8 @@ def mapdl(request, tmpdir_factory): mapdl._show_matplotlib_figures = False # CI: don't show matplotlib figures MAPDL_VERSION = mapdl.version # Caching version + VALID_PORTS.append(mapdl.port) + if ON_CI: mapdl._local = ON_LOCAL # CI: override for testing @@ -526,6 +572,7 @@ def mapdl(request, tmpdir_factory): # using yield rather than return here to be able to test exit yield mapdl + VALID_PORTS.remove(mapdl.port) ########################################################################### # test exit: only when allowed to start PYMAPDL ########################################################################### diff --git a/tests/test_cli.py b/tests/test_cli.py index a9499042bc..2f11ecc9a2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -26,7 +26,12 @@ import psutil import pytest -from conftest import requires +from conftest import VALID_PORTS, requires + +if VALID_PORTS: + PORT1 = max(VALID_PORTS) + 1 +else: + PORT1 = 50090 @pytest.fixture @@ -63,29 +68,23 @@ def test_launch_mapdl_cli(monkeypatch, run_cli, start_instance): monkeypatch.delenv("PYMAPDL_START_INSTANCE", raising=False) # Setting a port so it does not collide with the already running instance for testing - output = run_cli("start --port 50053") + output = run_cli(f"start --port {PORT1}") assert "Success: Launched an MAPDL instance " in output - assert "50053" in output + assert str(PORT1) in output # grab ips and port pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) - output = run_cli(f"stop --pid {pid}") - - try: - p = psutil.Process(pid) - assert not p.status() - except: - # An exception means the process is dead? - pass + output = run_cli(f"stop --port {PORT1}") + assert "success" in output.lower() @requires("click") @requires("local") @requires("nostudent") def test_launch_mapdl_cli_config(run_cli): - cmds_ = ["start", "--port 50090", "--jobname myjob"] + cmds_ = ["start", f"--port {PORT1}", "--jobname myjob"] cmd_warnings = [ "ip", "license_server_check", @@ -108,33 +107,46 @@ def test_launch_mapdl_cli_config(run_cli): cmd = cmd + " " + " ".join(cmd_warnings_) - output = run_cli(cmd) + try: + output = run_cli(cmd) - assert "Launched an MAPDL instance" in output - assert "50090" in output + assert "Launched an MAPDL instance" in output + assert str(PORT1) in output - # assert warnings - for each in cmd_warnings: - assert ( - f"The following argument is not allowed in CLI: '{each}'" in output - ), f"Warning about '{each}' not printed" + # assert warnings + for each in cmd_warnings: + assert ( + f"The following argument is not allowed in CLI: '{each}'" in output + ), f"Warning about '{each}' not printed" - # grab ips and port - pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) - p = psutil.Process(pid) - cmdline = " ".join(p.cmdline()) + # grab ips and port + pid = int(re.search(r"\(PID=(\d+)\)", output).groups()[0]) + p = psutil.Process(pid) + cmdline = " ".join(p.cmdline()) - assert "50090" in cmdline - assert "myjob" in cmdline + assert str(PORT1) in cmdline + assert "myjob" in cmdline - run_cli(f"stop --pid {pid}") + finally: + output = run_cli(f"stop --port {PORT1}") + assert "Success" in output + assert ( + f"Success: Ansys instances running on port {PORT1} have been stopped" + in output + ) @requires("click") @requires("local") @requires("nostudent") -@pytest.mark.xfail(reason="Flaky test") +@pytest.mark.xfail(reason="Flaky test. See #2435") def test_launch_mapdl_cli_list(run_cli): + + output = run_cli(f"start --port {PORT1}") + + assert "Success: Launched an MAPDL instance " in output + assert str(PORT1) in output + output = run_cli("list") assert "running" in output or "sleeping" in output assert "Is Instance" in output @@ -169,6 +181,13 @@ def test_launch_mapdl_cli_list(run_cli): assert len(output.splitlines()) > 2 assert "ansys" in output.lower() or "mapdl" in output.lower() + output = run_cli(f"stop --port {PORT1}") + assert "Success" in output + assert str(PORT1) in output + assert ( + f"Success: Ansys instances running on port {PORT1} have been stopped" in output + ) + @requires("click") def test_convert(run_cli, tmpdir): @@ -207,7 +226,7 @@ def test_convert(run_cli, tmpdir): @requires("click") def test_convert_pipe(): - cmd = """echo "/prep7" | pymapdl convert """ + cmd = """echo /prep7 | pymapdl convert """ out = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE) stdout = out.stdout.read().decode() diff --git a/tests/test_console.py b/tests/test_console.py index 999482f1fd..78aa29c308 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -616,3 +616,29 @@ def test_mode_console(mapdl_console): assert not mapdl_console.is_grpc assert not mapdl_console.is_corba assert mapdl_console.is_console + + +@requires("console") +def test_console_apdl_logging_start(tmpdir): + filename = str(tmpdir.mkdir("tmpdir").join("tmp.inp")) + + mapdl = pymapdl.launch_mapdl(log_apdl=filename, mode="console") + + mapdl.prep7() + mapdl.run("!comment test") + mapdl.k(1, 0, 0, 0) + mapdl.k(2, 1, 0, 0) + mapdl.k(3, 1, 1, 0) + mapdl.k(4, 0, 1, 0) + + mapdl.exit() + + with open(filename, "r") as fid: + text = "".join(fid.readlines()) + + assert "PREP7" in text + assert "!comment test" in text + assert "K,1,0,0,0" in text + assert "K,2,1,0,0" in text + assert "K,3,1,1,0" in text + assert "K,4,0,1,0" in text diff --git a/tests/test_mapdl.py b/tests/test_mapdl.py index 5961dfedc1..ce9a772930 100644 --- a/tests/test_mapdl.py +++ b/tests/test_mapdl.py @@ -34,7 +34,7 @@ import psutil import pytest -from conftest import has_dependency +from conftest import VALID_PORTS, has_dependency if has_dependency("pyvista"): from pyvista import MultiBlock @@ -60,6 +60,12 @@ PATH = os.path.dirname(os.path.abspath(__file__)) test_files = os.path.join(PATH, "test_files") + +if VALID_PORTS: + PORT1 = max(VALID_PORTS) + 1 +else: + PORT1 = 50090 + DEPRECATED_COMMANDS = [ "edasmp", "edbound", @@ -571,32 +577,6 @@ def test_apdl_logging_start(tmpdir, mapdl): mapdl._close_apdl_log() -@requires("console") -def test_console_apdl_logging_start(tmpdir): - filename = str(tmpdir.mkdir("tmpdir").join("tmp.inp")) - - mapdl = launch_mapdl(log_apdl=filename, mode="console") - - mapdl.prep7() - mapdl.run("!comment test") - mapdl.k(1, 0, 0, 0) - mapdl.k(2, 1, 0, 0) - mapdl.k(3, 1, 1, 0) - mapdl.k(4, 0, 1, 0) - - mapdl.exit() - - with open(filename, "r") as fid: - text = "".join(fid.readlines()) - - assert "PREP7" in text - assert "!comment test" in text - assert "K,1,0,0,0" in text - assert "K,2,1,0,0" in text - assert "K,3,1,1,0" in text - assert "K,4,0,1,0" in text - - def test_apdl_logging(mapdl, tmpdir): tmp_dir = tmpdir.mkdir("tmpdir") file_name = "tmp_logger.log" @@ -1948,12 +1928,12 @@ def test_igesin_whitespace(mapdl, cleared, tmpdir): @requires("local") @requires("nostudent") -@pytest.mark.xfail(reason="Flaky test") +@pytest.mark.xfail(reason="Save on exit is broken.") def test_save_on_exit(mapdl, cleared): mapdl2 = launch_mapdl( license_server_check=False, additional_switches=QUICK_LAUNCH_SWITCHES, - port=mapdl.port + 2, + port=PORT1, ) mapdl2.parameters["my_par"] = "initial_value" @@ -1970,7 +1950,7 @@ def test_save_on_exit(mapdl, cleared): mapdl2 = launch_mapdl( license_server_check=False, additional_switches=QUICK_LAUNCH_SWITCHES, - port=mapdl.port + 2, + port=PORT1, ) mapdl2.resume(db_path) if mapdl.version >= 24.2: @@ -1989,10 +1969,12 @@ def test_save_on_exit(mapdl, cleared): mapdl2 = launch_mapdl( license_server_check=False, additional_switches=QUICK_LAUNCH_SWITCHES, - port=mapdl.port + 2, + port=PORT1, ) mapdl2.resume(db_path) assert mapdl2.parameters["my_par"] == "new_initial_value" + + # cleaning up mapdl2.exit(force=True) @@ -2311,6 +2293,7 @@ def test_use_vtk(mapdl): @requires("local") +@pytest.mark.xfail(reason="Flaky test. See #2435") def test__remove_temp_dir_on_exit(mapdl, tmpdir): path = os.path.join(tempfile.gettempdir(), "ansys_" + random_string()) os.makedirs(path) @@ -2331,18 +2314,19 @@ def test__remove_temp_dir_on_exit(mapdl, tmpdir): @requires("local") @requires("nostudent") -@pytest.mark.xfail(reason="Flaky test") +@pytest.mark.xfail(reason="Flaky test. See #2435") def test_remove_temp_dir_on_exit(mapdl): - mapdl_2 = launch_mapdl(remove_temp_dir_on_exit=True, port=mapdl.port + 2) + mapdl_2 = launch_mapdl(remove_temp_dir_on_exit=True, port=PORT1) path_ = mapdl_2.directory assert os.path.exists(path_) - assert all([psutil.pid_exists(pid) for pid in mapdl_2._pids]) # checking pids too + + pids = mapdl_2._pids + assert all([psutil.pid_exists(pid) for pid in pids]) # checking pids too mapdl_2.exit() - time.sleep(1.0) assert not os.path.exists(path_) - assert not all([psutil.pid_exists(pid) for pid in mapdl_2._pids]) + assert not all([psutil.pid_exists(pid) for pid in pids]) def test_sys(mapdl): diff --git a/tests/test_pool.py b/tests/test_pool.py index 2174c91985..20b4761790 100644 --- a/tests/test_pool.py +++ b/tests/test_pool.py @@ -28,7 +28,7 @@ import numpy as np import pytest -from conftest import ON_LOCAL, ON_STUDENT, START_INSTANCE, has_dependency +from conftest import ON_LOCAL, ON_STUDENT, has_dependency if has_dependency("ansys-tools-path"): from ansys.tools.path import find_ansys @@ -41,7 +41,7 @@ from ansys.mapdl.core import Mapdl, MapdlPool, examples from ansys.mapdl.core.errors import VersionError from ansys.mapdl.core.launcher import LOCALHOST, MAPDL_DEFAULT_PORT -from conftest import QUICK_LAUNCH_SWITCHES, NullContext, requires +from conftest import QUICK_LAUNCH_SWITCHES, VALID_PORTS, NullContext, requires # skip entire module unless HAS_GRPC pytestmark = requires("grpc") @@ -67,7 +67,7 @@ @pytest.fixture(scope="module") -def pool(tmpdir_factory): +def pool_creator(tmpdir_factory): run_path = str(tmpdir_factory.mktemp("ansys_pool")) port = os.environ.get("PYMAPDL_PORT", 50056) @@ -96,11 +96,16 @@ def pool(tmpdir_factory): wait=True, ) + VALID_PORTS.extend(mapdl_pool._ports) + yield mapdl_pool + for each in mapdl_pool._ports: + VALID_PORTS.remove(each) + ########################################################################## # test exit - mapdl_pool.exit() + mapdl_pool.exit(block=True) timeout = time.time() + TWAIT @@ -118,6 +123,13 @@ def pool(tmpdir_factory): assert not list(Path(pth).rglob("*.page*")) +@pytest.fixture +def pool(pool_creator): + # Checks whether the pool is fine before testing + pool_creator.wait_for_ready() + return pool_creator + + @skip_requires_194 def test_invalid_exec(): with pytest.raises(VersionError): @@ -129,7 +141,6 @@ def test_invalid_exec(): ) -# @pytest.mark.xfail(strict=False, reason="Flaky test. See #2435") def test_heal(pool): pool_sz = len(pool) pool_names = pool._names # copy pool names @@ -158,6 +169,7 @@ def test_simple_map(pool): @skip_if_ignore_pool @requires("local") +@pytest.mark.xfail(reason="Flaky test. See #2435") def test_map_timeout(pool): pool_sz = len(pool) @@ -176,12 +188,7 @@ def func(mapdl, tsleep): # the timeout option kills the MAPDL instance when we reach the timeout. # Let's wait for the pool to heal before continuing - timeout = time.time() + TWAIT - while len(pool) < pool_sz: - time.sleep(0.1) - if time.time() > timeout: - raise TimeoutError(f"Failed to restart instance in {TWAIT} seconds") - + pool.wait_for_ready(TWAIT) assert len(pool) == pool_sz @@ -191,21 +198,21 @@ def test_simple(pool): def func(mapdl): mapdl.clear() + return 1 + + outs = pool.map(func, wait=True) - outs = pool.map(func) assert len(outs) == len(pool) assert len(pool) == pool_sz -# fails intermittently @skip_if_ignore_pool def test_batch(pool): - input_files = [examples.vmfiles["vm%d" % i] for i in range(1, len(pool) + 3)] + input_files = [examples.vmfiles["vm%d" % i] for i in range(1, len(pool) + 1)] outputs = pool.run_batch(input_files) assert len(outputs) == len(input_files) -# fails intermittently @skip_if_ignore_pool def test_map(pool): completed_indices = [] @@ -225,9 +232,7 @@ def func(mapdl, input_file, index): @skip_if_ignore_pool -@pytest.mark.skipif( - not START_INSTANCE, reason="This test requires the pool to be local" -) +@requires("local") def test_abort(pool, tmpdir): pool_sz = len(pool) # initial pool size @@ -235,7 +240,7 @@ def test_abort(pool, tmpdir): tmp_file = str(tmpdir.join("woa.inp")) with open(tmp_file, "w") as f: - f.write("EXIT") + f.write("PREP7") input_files = [examples.vmfiles["vm%d" % i] for i in range(1, 11)] input_files += [tmp_file] @@ -269,6 +274,17 @@ def test_directory_names_default(pool): assert f"Instance_{i}" in dirs_path_pool +@skip_if_ignore_pool +def test_directory_names_default_with_restart(pool): + pool[1].exit() + pool.wait_for_ready() + + dirs_path_pool = os.listdir(pool._root_dir) + for i, _ in enumerate(pool._instances): + assert pool._names(i) in dirs_path_pool + assert f"Instance_{i}" in dirs_path_pool + + @requires("local") @skip_if_ignore_pool def test_directory_names_custom_string(tmpdir): @@ -328,9 +344,10 @@ def test_num_instances(): @skip_if_ignore_pool -def test_only_one_instance(): +def test_only_one_instance(mapdl): pool = MapdlPool( 1, + port=mapdl.port + 1, exec_file=EXEC_FILE, nproc=NPROC, additional_switches=QUICK_LAUNCH_SWITCHES, @@ -401,15 +418,17 @@ def test_next_with_returns_index(pool): assert not each_instance._busy -def test_multiple_ips(): +def test_multiple_ips(monkeypatch): ips = [ - "123.45.67.01", - "123.45.67.02", - "123.45.67.03", - "123.45.67.04", - "123.45.67.05", + "123.45.67.1", + "123.45.67.2", + "123.45.67.3", + "123.45.67.4", + "123.45.67.5", ] + monkeypatch.delenv("PYMAPDL_MAPDL_EXEC", raising=False) + conf = MapdlPool(ip=ips, _debug_no_launch=True)._debug_no_launch ips = [socket.gethostbyname(each) for each in ips] @@ -574,6 +593,9 @@ def test_multiple_ips(): [LOCALHOST, LOCALHOST], [MAPDL_DEFAULT_PORT, MAPDL_DEFAULT_PORT + 1], NullContext(), + marks=pytest.mark.xfail( + reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + ), ), pytest.param( 3, @@ -583,6 +605,9 @@ def test_multiple_ips(): [LOCALHOST, LOCALHOST, LOCALHOST], [MAPDL_DEFAULT_PORT, MAPDL_DEFAULT_PORT + 1, MAPDL_DEFAULT_PORT + 2], NullContext(), + marks=pytest.mark.xfail( + reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + ), ), pytest.param( 3, @@ -592,6 +617,9 @@ def test_multiple_ips(): [LOCALHOST, LOCALHOST, LOCALHOST], [50053, 50053 + 1, 50053 + 2], NullContext(), + marks=pytest.mark.xfail( + reason="Available ports cannot does not start in `MAPDL_DEFAULT_PORT`. Probably because there are other instances running already." + ), ), pytest.param( 3, @@ -749,9 +777,8 @@ def test_ip_port_n_instance( ): monkeypatch.delenv("PYMAPDL_START_INSTANCE", raising=False) monkeypatch.delenv("PYMAPDL_IP", raising=False) - monkeypatch.setenv( - "PYMAPDL_MAPDL_EXEC", "/ansys_inc/v222/ansys/bin/ansys222" - ) # to avoid trying to find it. + monkeypatch.delenv("PYMAPDL_PORT", raising=False) + monkeypatch.setenv("PYMAPDL_MAPDL_EXEC", "/ansys_inc/v222/ansys/bin/ansys222") with context: conf = MapdlPool(