|
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
4 | 4 |
|
| 5 | +import os |
| 6 | +import subprocess |
| 7 | +import sys |
| 8 | +import textwrap |
| 9 | +from pathlib import Path |
| 10 | + |
5 | 11 | import pytest |
6 | 12 |
|
7 | 13 | from cuda.pathfinder._dynamic_libs import load_nvidia_dynamic_lib as load_mod |
|
24 | 30 |
|
25 | 31 | _MODULE = "cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib" |
26 | 32 | _STEPS_MODULE = "cuda.pathfinder._dynamic_libs.search_steps" |
| 33 | +_PACKAGE_ROOT = Path(load_mod.__file__).resolve().parents[3] |
27 | 34 |
|
28 | 35 |
|
29 | 36 | def _ctx(libname: str = "nvvm") -> SearchContext: |
@@ -184,53 +191,120 @@ def test_try_via_ctk_root_regular_lib(tmp_path): |
184 | 191 |
|
185 | 192 |
|
186 | 193 | def test_subprocess_probe_returns_abs_path_on_string_payload(mocker): |
187 | | - result = mocker.Mock(stdout='"/usr/local/cuda/lib64/libcudart.so.13"\n') |
188 | | - run_mock = mocker.patch(f"{_MODULE}.run_in_spawned_child_process", return_value=result) |
| 194 | + result = subprocess.CompletedProcess( |
| 195 | + args=[], |
| 196 | + returncode=0, |
| 197 | + stdout='"/usr/local/cuda/lib64/libcudart.so.13"\n', |
| 198 | + stderr="", |
| 199 | + ) |
| 200 | + run_mock = mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
189 | 201 |
|
190 | 202 | assert _resolve_system_loaded_abs_path_in_subprocess("cudart") == "/usr/local/cuda/lib64/libcudart.so.13" |
191 | | - assert run_mock.call_args.kwargs.get("rethrow") is True |
| 203 | + run_mock.assert_called_once_with( |
| 204 | + [sys.executable, "-m", "cuda.pathfinder._dynamic_libs.canary_probe_subprocess", "cudart"], |
| 205 | + capture_output=True, |
| 206 | + text=True, |
| 207 | + timeout=10.0, |
| 208 | + check=False, |
| 209 | + cwd=_PACKAGE_ROOT, |
| 210 | + ) |
192 | 211 |
|
193 | 212 |
|
194 | 213 | def test_subprocess_probe_returns_none_on_null_payload(mocker): |
195 | | - result = mocker.Mock(stdout="null\n") |
196 | | - mocker.patch(f"{_MODULE}.run_in_spawned_child_process", return_value=result) |
| 214 | + result = subprocess.CompletedProcess(args=[], returncode=0, stdout="null\n", stderr="") |
| 215 | + mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
197 | 216 |
|
198 | 217 | assert _resolve_system_loaded_abs_path_in_subprocess("cudart") is None |
199 | 218 |
|
200 | 219 |
|
201 | 220 | def test_subprocess_probe_raises_on_child_failure(mocker): |
| 221 | + result = subprocess.CompletedProcess(args=[], returncode=1, stdout="", stderr="child failed\n") |
| 222 | + mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
| 223 | + |
| 224 | + with pytest.raises(ChildProcessError, match="child failed"): |
| 225 | + _resolve_system_loaded_abs_path_in_subprocess("cudart") |
| 226 | + |
| 227 | + |
| 228 | +def test_subprocess_probe_raises_on_timeout(mocker): |
202 | 229 | mocker.patch( |
203 | | - f"{_MODULE}.run_in_spawned_child_process", |
204 | | - side_effect=ChildProcessError("child failed"), |
| 230 | + f"{_MODULE}.subprocess.run", |
| 231 | + side_effect=subprocess.TimeoutExpired(cmd=["python"], timeout=10.0, stderr="probe hung\n"), |
205 | 232 | ) |
206 | | - with pytest.raises(ChildProcessError, match="child failed"): |
| 233 | + with pytest.raises(ChildProcessError, match="timed out after 10.0 seconds"): |
207 | 234 | _resolve_system_loaded_abs_path_in_subprocess("cudart") |
208 | 235 |
|
209 | 236 |
|
210 | 237 | def test_subprocess_probe_raises_on_empty_stdout(mocker): |
211 | | - result = mocker.Mock(stdout=" \n \n") |
212 | | - mocker.patch(f"{_MODULE}.run_in_spawned_child_process", return_value=result) |
| 238 | + result = subprocess.CompletedProcess(args=[], returncode=0, stdout=" \n \n", stderr="") |
| 239 | + mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
213 | 240 |
|
214 | 241 | with pytest.raises(RuntimeError, match="produced no stdout payload"): |
215 | 242 | _resolve_system_loaded_abs_path_in_subprocess("cudart") |
216 | 243 |
|
217 | 244 |
|
218 | 245 | def test_subprocess_probe_raises_on_invalid_json_payload(mocker): |
219 | | - result = mocker.Mock(stdout="not-json\n") |
220 | | - mocker.patch(f"{_MODULE}.run_in_spawned_child_process", return_value=result) |
| 246 | + result = subprocess.CompletedProcess(args=[], returncode=0, stdout="not-json\n", stderr="") |
| 247 | + mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
221 | 248 |
|
222 | 249 | with pytest.raises(RuntimeError, match="invalid JSON payload"): |
223 | 250 | _resolve_system_loaded_abs_path_in_subprocess("cudart") |
224 | 251 |
|
225 | 252 |
|
226 | 253 | def test_subprocess_probe_raises_on_unexpected_json_payload(mocker): |
227 | | - result = mocker.Mock(stdout='{"path": "/usr/local/cuda/lib64/libcudart.so.13"}\n') |
228 | | - mocker.patch(f"{_MODULE}.run_in_spawned_child_process", return_value=result) |
| 254 | + result = subprocess.CompletedProcess( |
| 255 | + args=[], |
| 256 | + returncode=0, |
| 257 | + stdout='{"path": "/usr/local/cuda/lib64/libcudart.so.13"}\n', |
| 258 | + stderr="", |
| 259 | + ) |
| 260 | + mocker.patch(f"{_MODULE}.subprocess.run", return_value=result) |
229 | 261 |
|
230 | 262 | with pytest.raises(RuntimeError, match="unexpected payload"): |
231 | 263 | _resolve_system_loaded_abs_path_in_subprocess("cudart") |
232 | 264 |
|
233 | 265 |
|
| 266 | +def test_subprocess_probe_does_not_reenter_calling_script(tmp_path): |
| 267 | + script_path = tmp_path / "call_probe.py" |
| 268 | + run_count_path = tmp_path / "run_count.txt" |
| 269 | + script_path.write_text( |
| 270 | + textwrap.dedent( |
| 271 | + f""" |
| 272 | + from pathlib import Path |
| 273 | +
|
| 274 | + from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import ( |
| 275 | + _resolve_system_loaded_abs_path_in_subprocess, |
| 276 | + ) |
| 277 | +
|
| 278 | + marker_path = Path({str(run_count_path)!r}) |
| 279 | + run_count = int(marker_path.read_text()) if marker_path.exists() else 0 |
| 280 | + marker_path.write_text(str(run_count + 1)) |
| 281 | +
|
| 282 | + try: |
| 283 | + _resolve_system_loaded_abs_path_in_subprocess("not_a_real_lib") |
| 284 | + except Exception: |
| 285 | + pass |
| 286 | + """ |
| 287 | + ), |
| 288 | + encoding="utf-8", |
| 289 | + ) |
| 290 | + env = os.environ.copy() |
| 291 | + existing_pythonpath = env.get("PYTHONPATH") |
| 292 | + env["PYTHONPATH"] = ( |
| 293 | + str(_PACKAGE_ROOT) if not existing_pythonpath else os.pathsep.join((str(_PACKAGE_ROOT), existing_pythonpath)) |
| 294 | + ) |
| 295 | + |
| 296 | + result = subprocess.run( # noqa: S603 - trusted argv: current interpreter + temp script created by this test |
| 297 | + [sys.executable, str(script_path)], |
| 298 | + capture_output=True, |
| 299 | + text=True, |
| 300 | + check=False, |
| 301 | + env=env, |
| 302 | + ) |
| 303 | + |
| 304 | + assert result.returncode == 0, result.stderr |
| 305 | + assert run_count_path.read_text(encoding="utf-8") == "1" |
| 306 | + |
| 307 | + |
234 | 308 | # --------------------------------------------------------------------------- |
235 | 309 | # _try_ctk_root_canary |
236 | 310 | # --------------------------------------------------------------------------- |
|
0 commit comments