|
26 | 26 | import subprocess |
27 | 27 | import tempfile |
28 | 28 | from time import sleep |
29 | | -from unittest.mock import patch |
| 29 | +from unittest.mock import MagicMock, Mock, patch |
30 | 30 | import warnings |
31 | 31 |
|
32 | 32 | import psutil |
@@ -2129,3 +2129,249 @@ def test_handle_launch_exceptions(msg, match, exception_type): |
2129 | 2129 | exception = exception_type(msg) |
2130 | 2130 | with pytest.raises(exception_type, match=match): |
2131 | 2131 | raise handle_launch_exceptions(exception) |
| 2132 | + |
| 2133 | + |
| 2134 | +def test_env_vars_propagation_in_launch_mapdl(): |
| 2135 | + """Test that env_vars are propagated to start_parm in launch_mapdl.""" |
| 2136 | + env_vars = {"MY_VAR": "test_value", "ANOTHER_VAR": "another_value"} |
| 2137 | + |
| 2138 | + args = launch_mapdl( |
| 2139 | + env_vars=env_vars, |
| 2140 | + _debug_no_launch=True, |
| 2141 | + ) |
| 2142 | + |
| 2143 | + # Check that env_vars are in the returned args |
| 2144 | + assert "env_vars" in args |
| 2145 | + assert args["env_vars"]["MY_VAR"] == "test_value" |
| 2146 | + assert args["env_vars"]["ANOTHER_VAR"] == "another_value" |
| 2147 | + |
| 2148 | + |
| 2149 | +def test_env_vars_with_slurm_bootstrap(monkeypatch): |
| 2150 | + """Test that SLURM env_vars are set correctly when launch_on_hpc is True.""" |
| 2151 | + # This test verifies that when replace_env_vars is used with launch_on_hpc, |
| 2152 | + # SLURM-specific environment variables are added |
| 2153 | + monkeypatch.delenv("PYMAPDL_START_INSTANCE", False) |
| 2154 | + |
| 2155 | + env_vars_input = {"CUSTOM_VAR": "custom_value"} |
| 2156 | + |
| 2157 | + # Capture what env_vars are passed to launch_grpc |
| 2158 | + captured_env_vars = None |
| 2159 | + |
| 2160 | + def mock_launch_grpc(cmd, run_location, env_vars=None, **kwargs): |
| 2161 | + nonlocal captured_env_vars |
| 2162 | + captured_env_vars = env_vars |
| 2163 | + # Mock process object |
| 2164 | + from tests.test_launcher import get_fake_process |
| 2165 | + |
| 2166 | + return get_fake_process("Submitted batch job 1001") |
| 2167 | + |
| 2168 | + with ( |
| 2169 | + patch("ansys.mapdl.core.launcher.launch_grpc", mock_launch_grpc), |
| 2170 | + patch("ansys.mapdl.core.launcher.send_scontrol") as mock_scontrol, |
| 2171 | + patch("ansys.mapdl.core.launcher.kill_job"), |
| 2172 | + ): |
| 2173 | + # Mock scontrol to avoid timeout |
| 2174 | + mock_scontrol.return_value = get_fake_process( |
| 2175 | + "JobState=RUNNING\nBatchHost=testhost\n" |
| 2176 | + ) |
| 2177 | + |
| 2178 | + try: |
| 2179 | + launch_mapdl( |
| 2180 | + launch_on_hpc=True, |
| 2181 | + replace_env_vars=env_vars_input, # Use replace_env_vars instead of env_vars |
| 2182 | + exec_file="/fake/path/to/ansys242", |
| 2183 | + nproc=2, |
| 2184 | + ) |
| 2185 | + except Exception: # nosec B703 |
| 2186 | + # We expect this to fail, we just want to capture env_vars |
| 2187 | + pass |
| 2188 | + |
| 2189 | + # Verify the env_vars that were passed to launch_grpc |
| 2190 | + assert captured_env_vars is not None |
| 2191 | + assert "CUSTOM_VAR" in captured_env_vars |
| 2192 | + assert captured_env_vars["CUSTOM_VAR"] == "custom_value" |
| 2193 | + assert captured_env_vars["ANS_MULTIPLE_NODES"] == "1" |
| 2194 | + assert captured_env_vars["HYDRA_BOOTSTRAP"] == "slurm" |
| 2195 | + |
| 2196 | + |
| 2197 | +def test_mapdl_grpc_launch_uses_provided_start_parm(): |
| 2198 | + """Test that MapdlGrpc._launch uses provided start_parm over instance _start_parm.""" |
| 2199 | + from ansys.mapdl.core.mapdl_grpc import MapdlGrpc |
| 2200 | + |
| 2201 | + # Create mock instance |
| 2202 | + mapdl_grpc = Mock(spec=MapdlGrpc) |
| 2203 | + mapdl_grpc._exited = True |
| 2204 | + mapdl_grpc._local = True # Add _local attribute |
| 2205 | + mapdl_grpc._start_parm = { |
| 2206 | + "exec_file": "/original/path/to/ansys242", |
| 2207 | + "jobname": "original_job", |
| 2208 | + "nproc": 2, |
| 2209 | + "ram": 1024, |
| 2210 | + "port": 50052, |
| 2211 | + "additional_switches": "", |
| 2212 | + "mode": "grpc", |
| 2213 | + "run_location": "/default/run/location", |
| 2214 | + } |
| 2215 | + mapdl_grpc._env_vars = None |
| 2216 | + mapdl_grpc._connect = MagicMock() |
| 2217 | + mapdl_grpc._mapdl_process = None # Add _mapdl_process attribute |
| 2218 | + |
| 2219 | + # Custom start_parm that should be used |
| 2220 | + custom_start_parm = { |
| 2221 | + "exec_file": "/custom/path/to/ansys242", |
| 2222 | + "jobname": "custom_job", |
| 2223 | + "nproc": 4, |
| 2224 | + "ram": 2048, |
| 2225 | + "port": 50053, |
| 2226 | + "additional_switches": "-custom", |
| 2227 | + "mode": "grpc", |
| 2228 | + "env_vars": {"CUSTOM_VAR": "custom_value"}, |
| 2229 | + "run_location": "/custom/run/location", |
| 2230 | + } |
| 2231 | + |
| 2232 | + # Mock the launch_grpc function to capture what parameters are used |
| 2233 | + # Note: launch_grpc is in launcher module, not mapdl_grpc module |
| 2234 | + with patch("ansys.mapdl.core.launcher.launch_grpc") as mock_launch_grpc: |
| 2235 | + # Bind the real _launch method |
| 2236 | + mapdl_grpc._launch = MapdlGrpc._launch.__get__(mapdl_grpc, type(mapdl_grpc)) |
| 2237 | + |
| 2238 | + # Call _launch with custom start_parm |
| 2239 | + mapdl_grpc._launch(start_parm=custom_start_parm, timeout=10) |
| 2240 | + |
| 2241 | + # Verify launch_grpc was called |
| 2242 | + mock_launch_grpc.assert_called_once() |
| 2243 | + |
| 2244 | + # Get the cmd argument passed to launch_grpc |
| 2245 | + call_args = mock_launch_grpc.call_args |
| 2246 | + cmd_used = call_args[1]["cmd"] |
| 2247 | + |
| 2248 | + # Verify the command uses custom_start_parm values |
| 2249 | + assert "/custom/path/to/ansys242" in " ".join(cmd_used) |
| 2250 | + assert "custom_job" in " ".join(cmd_used) |
| 2251 | + |
| 2252 | + |
| 2253 | +def test_open_gui_with_mocked_call(mapdl, fake_local_mapdl): |
| 2254 | + """Test that open_gui uses the correct exec_file with mocked subprocess.call.""" |
| 2255 | + from contextlib import ExitStack |
| 2256 | + |
| 2257 | + custom_exec_file = "/custom/test/path/ansys242" |
| 2258 | + captured_call_args = None |
| 2259 | + |
| 2260 | + def mock_call(*args, **kwargs): |
| 2261 | + nonlocal captured_call_args |
| 2262 | + captured_call_args = args[0] if args else None |
| 2263 | + return 0 |
| 2264 | + |
| 2265 | + with ExitStack() as stack: |
| 2266 | + # Mock _local to True so open_gui doesn't raise "can only be called from local" |
| 2267 | + stack.enter_context(patch.object(mapdl, "_local", True)) |
| 2268 | + |
| 2269 | + # Mock pathlib.Path to return a mock that has is_file() return True |
| 2270 | + mock_path = MagicMock() |
| 2271 | + mock_path.is_file.return_value = True |
| 2272 | + stack.enter_context( |
| 2273 | + patch("ansys.mapdl.core.mapdl_core.pathlib.Path", return_value=mock_path) |
| 2274 | + ) |
| 2275 | + |
| 2276 | + # Mock the call function imported in mapdl_core |
| 2277 | + stack.enter_context( |
| 2278 | + patch("ansys.mapdl.core.mapdl_core.call", side_effect=mock_call) |
| 2279 | + ) |
| 2280 | + |
| 2281 | + # IMPORTANT: Mock exit, finish, save, _launch, resume to prevent killing the MAPDL instance |
| 2282 | + stack.enter_context(patch.object(mapdl, "exit")) |
| 2283 | + stack.enter_context(patch.object(mapdl, "finish")) |
| 2284 | + stack.enter_context(patch.object(mapdl, "save")) |
| 2285 | + stack.enter_context(patch.object(mapdl, "_cache_routine")) |
| 2286 | + stack.enter_context(patch.object(mapdl, "_launch")) |
| 2287 | + stack.enter_context(patch.object(mapdl, "resume")) |
| 2288 | + |
| 2289 | + try: |
| 2290 | + # Call open_gui with custom exec_file |
| 2291 | + mapdl.open_gui(exec_file=custom_exec_file, inplace=True) |
| 2292 | + except Exception: # nosec B703 |
| 2293 | + # open_gui might fail for various reasons after the call |
| 2294 | + # We're only interested in verifying the call arguments |
| 2295 | + pass |
| 2296 | + |
| 2297 | + # Verify that subprocess.call was called with the custom exec_file |
| 2298 | + assert captured_call_args is not None, "subprocess.call was not called" |
| 2299 | + assert ( |
| 2300 | + custom_exec_file in captured_call_args |
| 2301 | + ), f"Expected {custom_exec_file} in call args, but got {captured_call_args}" |
| 2302 | + assert "-g" in captured_call_args, "Expected -g flag for GUI mode" |
| 2303 | + assert mapdl.jobname in " ".join( |
| 2304 | + str(arg) for arg in captured_call_args |
| 2305 | + ), f"Expected jobname {mapdl.jobname} in call args" |
| 2306 | + |
| 2307 | + |
| 2308 | +def test_open_gui_complete_flow_with_mocked_methods(mapdl, fake_local_mapdl): |
| 2309 | + """Test complete open_gui flow: call, _launch, and reconnection methods are invoked.""" |
| 2310 | + |
| 2311 | + custom_exec_file = "/custom/test/path/ansys242" |
| 2312 | + |
| 2313 | + # Track what methods were called |
| 2314 | + call_invoked = False |
| 2315 | + launch_invoked = False |
| 2316 | + |
| 2317 | + def mock_call(*args, **kwargs): |
| 2318 | + nonlocal call_invoked |
| 2319 | + call_invoked = True |
| 2320 | + return 0 |
| 2321 | + |
| 2322 | + def mock_launch(start_parm, timeout=10): |
| 2323 | + nonlocal launch_invoked |
| 2324 | + launch_invoked = True |
| 2325 | + # Verify start_parm is passed |
| 2326 | + assert start_parm is not None |
| 2327 | + assert "exec_file" in start_parm |
| 2328 | + |
| 2329 | + # Mock the call function imported in mapdl_core |
| 2330 | + with patch("ansys.mapdl.core.mapdl_core.call", side_effect=mock_call): |
| 2331 | + # Mock _local to True so open_gui doesn't raise "can only be called from local" |
| 2332 | + with patch.object(mapdl, "_local", True): |
| 2333 | + # Mock pathlib.Path to return a mock that has is_file() return True |
| 2334 | + mock_path = MagicMock() |
| 2335 | + mock_path.is_file.return_value = True |
| 2336 | + with patch( |
| 2337 | + "ansys.mapdl.core.mapdl_core.pathlib.Path", return_value=mock_path |
| 2338 | + ): |
| 2339 | + # Store original _launch to restore later |
| 2340 | + original_launch = mapdl._launch |
| 2341 | + |
| 2342 | + try: |
| 2343 | + # Replace _launch with our mock |
| 2344 | + mapdl._launch = mock_launch |
| 2345 | + |
| 2346 | + # Mock methods that open_gui calls before and after |
| 2347 | + with ( |
| 2348 | + patch.object(mapdl, "finish") as mock_finish, |
| 2349 | + patch.object(mapdl, "save") as mock_save, |
| 2350 | + patch.object(mapdl, "exit") as mock_exit, |
| 2351 | + patch.object(mapdl, "resume") as mock_resume, |
| 2352 | + patch.object(mapdl, "_cache_routine") as mock_cache, |
| 2353 | + ): |
| 2354 | + try: |
| 2355 | + # Call open_gui with custom exec_file |
| 2356 | + mapdl.open_gui(exec_file=custom_exec_file, inplace=True) |
| 2357 | + except Exception: # nosec B703 |
| 2358 | + # Some methods might fail, but we verify they were called |
| 2359 | + pass |
| 2360 | + |
| 2361 | + # Verify the flow of method calls |
| 2362 | + assert ( |
| 2363 | + mock_finish.called |
| 2364 | + ), "finish() should be called before GUI" |
| 2365 | + assert mock_save.called, "save() should be called before GUI" |
| 2366 | + assert mock_exit.called, "exit() should be called before GUI" |
| 2367 | + assert call_invoked, "subprocess.call should be invoked for GUI" |
| 2368 | + assert launch_invoked, "_launch() should be called to reconnect" |
| 2369 | + assert ( |
| 2370 | + mock_resume.called |
| 2371 | + ), "resume() should be called after reconnection" |
| 2372 | + assert ( |
| 2373 | + mock_cache.called |
| 2374 | + ), "_cache_routine() should be called after reconnection" |
| 2375 | + finally: |
| 2376 | + # Restore original _launch |
| 2377 | + mapdl._launch = original_launch |
0 commit comments