Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/4270.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Open gui not working
1 change: 1 addition & 0 deletions src/ansys/mapdl/core/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -1690,6 +1690,7 @@ def launch_mapdl(
env_vars.setdefault("HYDRA_BOOTSTRAP", "slurm")

start_parm = generate_start_parameters(args)
start_parm["env_vars"] = env_vars

# Early exit for debugging.
if args["_debug_no_launch"]:
Expand Down
36 changes: 32 additions & 4 deletions src/ansys/mapdl/core/mapdl_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1723,7 +1723,12 @@ def _generate_iges(self):
self.igesout(filename, att=1, mute=True)
return filename

def open_gui(self, include_result=None, inplace=None): # pragma: no cover
def open_gui(
self,
include_result: bool | None = None,
inplace: bool | None = None,
exec_file: str | None = None,
): # pragma: no cover
"""Save the existing database and open it up in the MAPDL GUI.

Parameters
Expand All @@ -1735,9 +1740,15 @@ def open_gui(self, include_result=None, inplace=None): # pragma: no cover
inplace : bool, optional
Open the GUI on the current MAPDL working directory, instead of
creating a new temporary directory and coping the results files
over there. If ``True``, ignores ``include_result`` parameter. By
over there. If ``True``, ignores ``include_result`` parameter. By
default, this ``False``.

exec_file: str, optional
Path to the MAPDL executable. If not provided, it will try to obtain
it using the `ansys.tools.path` package. If this package is not
available, it will use the same executable as the current MAPDL
instance.

Examples
--------
>>> from ansys.mapdl.core import launch_mapdl
Expand Down Expand Up @@ -1767,7 +1778,7 @@ def open_gui(self, include_result=None, inplace=None): # pragma: no cover
>>> mapdl.eplot()
"""
# lazy load here to avoid circular import
from ansys.mapdl.core.launcher import get_mapdl_path
from ansys.mapdl.core import _HAS_ATP

if not self._local:
raise MapdlRuntimeError(
Expand Down Expand Up @@ -1844,7 +1855,24 @@ def open_gui(self, include_result=None, inplace=None): # pragma: no cover
# issue system command to run ansys in GUI mode
cwd = os.getcwd()
os.chdir(run_dir)
exec_file = self._start_parm.get("exec_file", get_mapdl_path(allow_input=False))

if not exec_file:
if _HAS_ATP:
from ansys.mapdl.core import get_mapdl_path

exec_file_ = get_mapdl_path(allow_input=False)
else:
exec_file_ = None

exec_file = self._start_parm.get("exec_file", exec_file_)

if not exec_file:
raise MapdlRuntimeError(
"The path to the MAPDL executable was not found. "
"Please set it using the 'exec_file' parameter when "
"launching MAPDL."
)

nproc = self._start_parm.get("nproc", 2)
add_sw = self._start_parm.get("additional_switches", "")

Expand Down
8 changes: 5 additions & 3 deletions src/ansys/mapdl/core/mapdl_grpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,9 @@ def _get_server_version(self) -> tuple[int, int, int]:
sver = version_string_as_tuple(verstr)
return sver

def _launch(self, start_parm, timeout=10):
def _launch(
self, start_parm: dict[str, Any] | None = None, timeout: int | None = 10
):
"""Launch a local session of MAPDL in gRPC mode.

This should only need to be used for legacy ``open_gui``
Expand All @@ -958,7 +960,7 @@ def _launch(self, start_parm, timeout=10):

self._exited = False # reset exit state

args = self._start_parm
args = start_parm or self._start_parm
cmd = generate_mapdl_launch_command(
exec_file=args["exec_file"],
jobname=args["jobname"],
Expand All @@ -972,7 +974,7 @@ def _launch(self, start_parm, timeout=10):
cmd=cmd, run_location=args["run_location"], env_vars=self._env_vars or None
)

self._connect(args["port"])
self._connect()

# may need to wait for viable connection in open_gui case
tmax = time.time() + timeout
Expand Down
248 changes: 247 additions & 1 deletion tests/test_launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import subprocess
import tempfile
from time import sleep
from unittest.mock import patch
from unittest.mock import MagicMock, Mock, patch
import warnings

import psutil
Expand Down Expand Up @@ -2129,3 +2129,249 @@ def test_handle_launch_exceptions(msg, match, exception_type):
exception = exception_type(msg)
with pytest.raises(exception_type, match=match):
raise handle_launch_exceptions(exception)


def test_env_vars_propagation_in_launch_mapdl():
"""Test that env_vars are propagated to start_parm in launch_mapdl."""
env_vars = {"MY_VAR": "test_value", "ANOTHER_VAR": "another_value"}

args = launch_mapdl(
env_vars=env_vars,
_debug_no_launch=True,
)

# Check that env_vars are in the returned args
assert "env_vars" in args
assert args["env_vars"]["MY_VAR"] == "test_value"
assert args["env_vars"]["ANOTHER_VAR"] == "another_value"


def test_env_vars_with_slurm_bootstrap(monkeypatch):
"""Test that SLURM env_vars are set correctly when launch_on_hpc is True."""
# This test verifies that when replace_env_vars is used with launch_on_hpc,
# SLURM-specific environment variables are added
monkeypatch.delenv("PYMAPDL_START_INSTANCE", False)

env_vars_input = {"CUSTOM_VAR": "custom_value"}

# Capture what env_vars are passed to launch_grpc
captured_env_vars = None

def mock_launch_grpc(cmd, run_location, env_vars=None, **kwargs):
nonlocal captured_env_vars
captured_env_vars = env_vars
# Mock process object
from tests.test_launcher import get_fake_process

return get_fake_process("Submitted batch job 1001")

with (
patch("ansys.mapdl.core.launcher.launch_grpc", mock_launch_grpc),
patch("ansys.mapdl.core.launcher.send_scontrol") as mock_scontrol,
patch("ansys.mapdl.core.launcher.kill_job"),
):
# Mock scontrol to avoid timeout
mock_scontrol.return_value = get_fake_process(
"JobState=RUNNING\nBatchHost=testhost\n"
)

try:
launch_mapdl(
launch_on_hpc=True,
replace_env_vars=env_vars_input, # Use replace_env_vars instead of env_vars
exec_file="/fake/path/to/ansys242",
nproc=2,
)
except Exception: # nosec B703
# We expect this to fail, we just want to capture env_vars
pass

# Verify the env_vars that were passed to launch_grpc
assert captured_env_vars is not None
assert "CUSTOM_VAR" in captured_env_vars
assert captured_env_vars["CUSTOM_VAR"] == "custom_value"
assert captured_env_vars["ANS_MULTIPLE_NODES"] == "1"
assert captured_env_vars["HYDRA_BOOTSTRAP"] == "slurm"


def test_mapdl_grpc_launch_uses_provided_start_parm():
"""Test that MapdlGrpc._launch uses provided start_parm over instance _start_parm."""
from ansys.mapdl.core.mapdl_grpc import MapdlGrpc

# Create mock instance
mapdl_grpc = Mock(spec=MapdlGrpc)
mapdl_grpc._exited = True
mapdl_grpc._local = True # Add _local attribute
mapdl_grpc._start_parm = {
"exec_file": "/original/path/to/ansys242",
"jobname": "original_job",
"nproc": 2,
"ram": 1024,
"port": 50052,
"additional_switches": "",
"mode": "grpc",
"run_location": "/default/run/location",
}
mapdl_grpc._env_vars = None
mapdl_grpc._connect = MagicMock()
mapdl_grpc._mapdl_process = None # Add _mapdl_process attribute

# Custom start_parm that should be used
custom_start_parm = {
"exec_file": "/custom/path/to/ansys242",
"jobname": "custom_job",
"nproc": 4,
"ram": 2048,
"port": 50053,
"additional_switches": "-custom",
"mode": "grpc",
"env_vars": {"CUSTOM_VAR": "custom_value"},
"run_location": "/custom/run/location",
}

# Mock the launch_grpc function to capture what parameters are used
# Note: launch_grpc is in launcher module, not mapdl_grpc module
with patch("ansys.mapdl.core.launcher.launch_grpc") as mock_launch_grpc:
# Bind the real _launch method
mapdl_grpc._launch = MapdlGrpc._launch.__get__(mapdl_grpc, type(mapdl_grpc))

# Call _launch with custom start_parm
mapdl_grpc._launch(start_parm=custom_start_parm, timeout=10)

# Verify launch_grpc was called
mock_launch_grpc.assert_called_once()

# Get the cmd argument passed to launch_grpc
call_args = mock_launch_grpc.call_args
cmd_used = call_args[1]["cmd"]

# Verify the command uses custom_start_parm values
assert "/custom/path/to/ansys242" in " ".join(cmd_used)
assert "custom_job" in " ".join(cmd_used)


def test_open_gui_with_mocked_call(mapdl, fake_local_mapdl):
"""Test that open_gui uses the correct exec_file with mocked subprocess.call."""
from contextlib import ExitStack

custom_exec_file = "/custom/test/path/ansys242"
captured_call_args = None

def mock_call(*args, **kwargs):
nonlocal captured_call_args
captured_call_args = args[0] if args else None
return 0

with ExitStack() as stack:
# Mock _local to True so open_gui doesn't raise "can only be called from local"
stack.enter_context(patch.object(mapdl, "_local", True))

# Mock pathlib.Path to return a mock that has is_file() return True
mock_path = MagicMock()
mock_path.is_file.return_value = True
stack.enter_context(
patch("ansys.mapdl.core.mapdl_core.pathlib.Path", return_value=mock_path)
)

# Mock the call function imported in mapdl_core
stack.enter_context(
patch("ansys.mapdl.core.mapdl_core.call", side_effect=mock_call)
)

# IMPORTANT: Mock exit, finish, save, _launch, resume to prevent killing the MAPDL instance
stack.enter_context(patch.object(mapdl, "exit"))
stack.enter_context(patch.object(mapdl, "finish"))
stack.enter_context(patch.object(mapdl, "save"))
stack.enter_context(patch.object(mapdl, "_cache_routine"))
stack.enter_context(patch.object(mapdl, "_launch"))
stack.enter_context(patch.object(mapdl, "resume"))

try:
# Call open_gui with custom exec_file
mapdl.open_gui(exec_file=custom_exec_file, inplace=True)
except Exception: # nosec B703
# open_gui might fail for various reasons after the call
# We're only interested in verifying the call arguments
pass

# Verify that subprocess.call was called with the custom exec_file
assert captured_call_args is not None, "subprocess.call was not called"
assert (
custom_exec_file in captured_call_args
), f"Expected {custom_exec_file} in call args, but got {captured_call_args}"
assert "-g" in captured_call_args, "Expected -g flag for GUI mode"
assert mapdl.jobname in " ".join(
str(arg) for arg in captured_call_args
), f"Expected jobname {mapdl.jobname} in call args"


def test_open_gui_complete_flow_with_mocked_methods(mapdl, fake_local_mapdl):
"""Test complete open_gui flow: call, _launch, and reconnection methods are invoked."""

custom_exec_file = "/custom/test/path/ansys242"

# Track what methods were called
call_invoked = False
launch_invoked = False

def mock_call(*args, **kwargs):
nonlocal call_invoked
call_invoked = True
return 0

def mock_launch(start_parm, timeout=10):
nonlocal launch_invoked
launch_invoked = True
# Verify start_parm is passed
assert start_parm is not None
assert "exec_file" in start_parm

# Mock the call function imported in mapdl_core
with patch("ansys.mapdl.core.mapdl_core.call", side_effect=mock_call):
# Mock _local to True so open_gui doesn't raise "can only be called from local"
with patch.object(mapdl, "_local", True):
# Mock pathlib.Path to return a mock that has is_file() return True
mock_path = MagicMock()
mock_path.is_file.return_value = True
with patch(
"ansys.mapdl.core.mapdl_core.pathlib.Path", return_value=mock_path
):
# Store original _launch to restore later
original_launch = mapdl._launch

try:
# Replace _launch with our mock
mapdl._launch = mock_launch

# Mock methods that open_gui calls before and after
with (
patch.object(mapdl, "finish") as mock_finish,
patch.object(mapdl, "save") as mock_save,
patch.object(mapdl, "exit") as mock_exit,
patch.object(mapdl, "resume") as mock_resume,
patch.object(mapdl, "_cache_routine") as mock_cache,
):
try:
# Call open_gui with custom exec_file
mapdl.open_gui(exec_file=custom_exec_file, inplace=True)
except Exception: # nosec B703
# Some methods might fail, but we verify they were called
pass

# Verify the flow of method calls
assert (
mock_finish.called
), "finish() should be called before GUI"
assert mock_save.called, "save() should be called before GUI"
assert mock_exit.called, "exit() should be called before GUI"
assert call_invoked, "subprocess.call should be invoked for GUI"
assert launch_invoked, "_launch() should be called to reconnect"
assert (
mock_resume.called
), "resume() should be called after reconnection"
assert (
mock_cache.called
), "_cache_routine() should be called after reconnection"
finally:
# Restore original _launch
mapdl._launch = original_launch
Loading