From 11cc1e09e0d35fd3593d7e77c639f924e9f74e67 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Thu, 13 Aug 2026 08:54:16 -0700 Subject: [PATCH] Refactor standalone Windows Nsight discovery --- .../_binaries/find_nvidia_binary_utility.py | 76 +----- .../pathfinder/_binaries/windows_nsight.py | 74 +++++ .../tests/test_find_nvidia_binaries.py | 254 +++--------------- cuda_pathfinder/tests/test_windows_nsight.py | 196 ++++++++++++++ 4 files changed, 316 insertions(+), 284 deletions(-) create mode 100644 cuda_pathfinder/cuda/pathfinder/_binaries/windows_nsight.py create mode 100644 cuda_pathfinder/tests/test_windows_nsight.py diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py index 28082507238..42dcfda1cfb 100644 --- a/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/find_nvidia_binary_utility.py @@ -2,29 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 import functools -import importlib import os from collections.abc import Iterable -from typing import Any -from cuda.pathfinder._binaries import supported_nvidia_binaries +from cuda.pathfinder._binaries import supported_nvidia_binaries, windows_nsight from cuda.pathfinder._utils.ctk_root_canary import CTK_ROOT_CANARY_ANCHOR_LIBNAMES from cuda.pathfinder._utils.env_vars import get_cuda_path_or_home from cuda.pathfinder._utils.find_sub_dirs import find_sub_dirs_all_sitepackages from cuda.pathfinder._utils.platform_aware import IS_WINDOWS -from cuda.pathfinder._utils.windows_arch import windows_machine_arch - -_NSIGHT_REGISTRY_ROOT = r"SOFTWARE\NVIDIA Corporation\Installed Products\Nsight" - -_NSYS_TARGET_DIR_BY_ARCH = { - "x64": "target-windows-x64", - "arm64": "target-windows-armv8", -} - -_NCU_TARGET_DIR_BY_ARCH = { - "x64": os.path.join("target", "windows-desktop-win7-x64"), - "arm64": os.path.join("target", "windows-desktop-win10-t23x-a64"), -} class UnsupportedBinaryError(Exception): @@ -83,61 +68,6 @@ def _find_windows_compute_sanitizer(ctk_root: str) -> str | None: ) -def _windows_installed_nsight_root(product: str) -> str | None: - """Return the active Nsight product installation recorded by its MSI.""" - # ``winreg`` attributes are absent from the type stubs on non-Windows hosts. - winreg: Any = importlib.import_module("winreg") - - access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY - product_key_path = rf"{_NSIGHT_REGISTRY_ROOT}\{product}" - try: - product_context = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) - except FileNotFoundError: - return None - - try: - with product_context as product_key: - current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion") - if not isinstance(current_version, str) or not current_version.strip(): - raise RuntimeError( - f"Invalid CurrentVersion value {current_version!r} in " - f"Nsight {product!r} registry registration at {product_key_path!r}" - ) - with winreg.OpenKey(product_key, current_version, 0, access) as version_key: - install_root, _ = winreg.QueryValueEx(version_key, None) - except FileNotFoundError as exc: - raise RuntimeError(f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}") from exc - - if not isinstance(install_root, str) or not install_root.strip(): - raise RuntimeError( - f"Invalid installation directory {install_root!r} in Nsight {product!r} " - f"registry registration at {product_key_path!r} version {current_version!r}" - ) - return install_root - - -def _find_windows_nsys() -> str | None: - install_root = _windows_installed_nsight_root("Systems") - if install_root is None: - return None - - target_dir = _NSYS_TARGET_DIR_BY_ARCH[windows_machine_arch()] - return _resolve_candidate_paths((os.path.join(install_root, target_dir, "nsys.exe"),)) - - -def _find_windows_ncu() -> str | None: - install_root = _windows_installed_nsight_root("Compute") - if install_root is None: - return None - - launcher = os.path.join(install_root, "ncu.bat") - if (found := _resolve_candidate_paths((launcher,))) is not None: - return found - - target_dir = _NCU_TARGET_DIR_BY_ARCH[windows_machine_arch()] - return _resolve_candidate_paths((os.path.join(install_root, target_dir, "ncu.exe"),)) - - def _resolve_ctk_root_via_canary() -> str | None: from cuda.pathfinder._dynamic_libs.load_nvidia_dynamic_lib import resolve_ctk_root_via_canary @@ -287,9 +217,9 @@ def find_nvidia_binary_utility(utility_name: str) -> str | None: # 3. Search library-specific standalone installations. # 3.1. Standalone Nsight CLI lookup is terminal; CTK does not contain nsys/ncu. if IS_WINDOWS and utility_name == "nsys": - return _find_windows_nsys() + return _resolve_candidate_paths(windows_nsight.nsys_candidate_paths()) if IS_WINDOWS and utility_name == "ncu": - return _find_windows_ncu() + return _resolve_candidate_paths(windows_nsight.ncu_candidate_paths()) # 3.2. Search in CUDA Toolkit (CUDA_PATH/CUDA_HOME). if (cuda_path := get_cuda_path_or_home()) is not None: diff --git a/cuda_pathfinder/cuda/pathfinder/_binaries/windows_nsight.py b/cuda_pathfinder/cuda/pathfinder/_binaries/windows_nsight.py new file mode 100644 index 00000000000..c5944c39e64 --- /dev/null +++ b/cuda_pathfinder/cuda/pathfinder/_binaries/windows_nsight.py @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import importlib +import os +from collections.abc import Iterator +from typing import Any + +from cuda.pathfinder._utils.windows_arch import windows_machine_arch + +_REGISTRY_ROOT = r"SOFTWARE\NVIDIA Corporation\Installed Products\Nsight" + +_NSYS_TARGET_DIR_BY_ARCH = { + "x64": "target-windows-x64", + "arm64": "target-windows-armv8", +} + +_NCU_TARGET_DIR_BY_ARCH = { + "x64": os.path.join("target", "windows-desktop-win7-x64"), + "arm64": os.path.join("target", "windows-desktop-win10-t23x-a64"), +} + + +def _installed_product_root(product: str) -> str | None: + """Return the active Nsight product installation recorded by its MSI.""" + # ``winreg`` attributes are absent from the type stubs on non-Windows hosts. + winreg: Any = importlib.import_module("winreg") + + access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY + product_key_path = rf"{_REGISTRY_ROOT}\{product}" + try: + product_context = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, product_key_path, 0, access) + except FileNotFoundError: + return None + + try: + with product_context as product_key: + current_version, _ = winreg.QueryValueEx(product_key, "CurrentVersion") + if not isinstance(current_version, str) or not current_version.strip(): + raise RuntimeError( + f"Invalid CurrentVersion value {current_version!r} in " + f"Nsight {product!r} registry registration at {product_key_path!r}" + ) + with winreg.OpenKey(product_key, current_version, 0, access) as version_key: + install_root, _ = winreg.QueryValueEx(version_key, None) + except FileNotFoundError as exc: + raise RuntimeError(f"Incomplete Nsight {product!r} registry registration at {product_key_path!r}") from exc + + if not isinstance(install_root, str) or not install_root.strip(): + raise RuntimeError( + f"Invalid installation directory {install_root!r} in Nsight {product!r} " + f"registry registration at {product_key_path!r} version {current_version!r}" + ) + return install_root + + +def nsys_candidate_paths() -> Iterator[str]: + install_root = _installed_product_root("Systems") + if install_root is None: + return + + target_dir = _NSYS_TARGET_DIR_BY_ARCH[windows_machine_arch()] + yield os.path.join(install_root, target_dir, "nsys.exe") + + +def ncu_candidate_paths() -> Iterator[str]: + install_root = _installed_product_root("Compute") + if install_root is None: + return + + yield os.path.join(install_root, "ncu.bat") + + target_dir = _NCU_TARGET_DIR_BY_ARCH[windows_machine_arch()] + yield os.path.join(install_root, target_dir, "ncu.exe") diff --git a/cuda_pathfinder/tests/test_find_nvidia_binaries.py b/cuda_pathfinder/tests/test_find_nvidia_binaries.py index 668be893863..9d08f5f4d5c 100644 --- a/cuda_pathfinder/tests/test_find_nvidia_binaries.py +++ b/cuda_pathfinder/tests/test_find_nvidia_binaries.py @@ -58,15 +58,6 @@ def fake_is_executable_candidate(path): return checked -def _patch_winreg(mocker): - winreg = mocker.MagicMock() - winreg.HKEY_LOCAL_MACHINE = object() - winreg.KEY_READ = 0x20019 - winreg.KEY_WOW64_64KEY = 0x0100 - mocker.patch.object(binary_finder_module.importlib, "import_module", return_value=winreg) - return winreg - - @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_search_path_includes_site_packages_conda_cuda(monkeypatch, mocker): conda_prefix = os.path.join(os.sep, "conda") @@ -193,71 +184,6 @@ def test_find_compute_sanitizer_uses_canary_ctk_root(monkeypatch, mocker): canary.assert_called_once_with() -@pytest.mark.parametrize( - ("machine_arch", "target_dir"), - ( - ("x64", "target-windows-x64"), - ("arm64", "target-windows-armv8"), - ), -) -@pytest.mark.agent_authored(model="gpt-5.6") -def test_find_windows_nsys_uses_machine_arch(mocker, machine_arch, target_dir): - install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") - expected = os.path.join(install_root, target_dir, "nsys.exe") - mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) - mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) - checked = _patch_exec_probe(mocker, existing=[expected]) - - assert binary_finder_module._find_windows_nsys() == os.path.abspath(expected) - assert checked == [expected] - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_find_windows_nsys_does_not_fallback_to_other_arch(mocker): - install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") - arm64 = os.path.join(install_root, "target-windows-armv8", "nsys.exe") - x64 = os.path.join(install_root, "target-windows-x64", "nsys.exe") - mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) - mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value="arm64") - checked = _patch_exec_probe(mocker, existing=[x64]) - - assert binary_finder_module._find_windows_nsys() is None - assert checked == [arm64] - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_find_windows_ncu_prefers_launcher(mocker): - install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") - launcher = os.path.join(install_root, "ncu.bat") - mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) - machine_arch_mock = mocker.patch.object(binary_finder_module, "windows_machine_arch") - checked = _patch_exec_probe(mocker, existing=[launcher]) - - assert binary_finder_module._find_windows_ncu() == os.path.abspath(launcher) - assert checked == [launcher] - machine_arch_mock.assert_not_called() - - -@pytest.mark.parametrize( - ("machine_arch", "target_dir"), - ( - ("x64", os.path.join("target", "windows-desktop-win7-x64")), - ("arm64", os.path.join("target", "windows-desktop-win10-t23x-a64")), - ), -) -@pytest.mark.agent_authored(model="gpt-5.6") -def test_find_windows_ncu_falls_back_to_machine_binary(mocker, machine_arch, target_dir): - install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") - launcher = os.path.join(install_root, "ncu.bat") - expected = os.path.join(install_root, target_dir, "ncu.exe") - mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=install_root) - mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) - checked = _patch_exec_probe(mocker, existing=[expected]) - - assert binary_finder_module._find_windows_ncu() == os.path.abspath(expected) - assert checked == [launcher, expected] - - @pytest.mark.parametrize( ("utility_name", "candidate_names"), ( @@ -276,8 +202,7 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker, mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) - registry_root = mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root") - machine_arch = mocker.patch.object(binary_finder_module, "windows_machine_arch") + candidate_paths = mocker.patch.object(binary_finder_module.windows_nsight, f"{utility_name}_candidate_paths") get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") checked = _patch_exec_probe(mocker, existing=[expected]) @@ -287,8 +212,7 @@ def test_find_binary_windows_nsight_conda_precedes_registry(monkeypatch, mocker, *(os.path.join(site_dir, name) for name in candidate_names), os.path.join(conda_bin, candidate_names[0]), ] - registry_root.assert_not_called() - machine_arch.assert_not_called() + candidate_paths.assert_not_called() get_cuda_path.assert_not_called() canary.assert_not_called() @@ -329,9 +253,11 @@ def test_find_binary_windows_nsight_composes_registry_and_native_target( mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) registry_root = mocker.patch.object( - binary_finder_module, "_windows_installed_nsight_root", return_value=install_root + binary_finder_module.windows_nsight, "_installed_product_root", return_value=install_root + ) + machine_arch_mock = mocker.patch.object( + binary_finder_module.windows_nsight, "windows_machine_arch", return_value=machine_arch ) - machine_arch_mock = mocker.patch.object(binary_finder_module, "windows_machine_arch", return_value=machine_arch) get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") checked = _patch_exec_probe(mocker, existing=[expected]) @@ -348,6 +274,31 @@ def test_find_binary_windows_nsight_composes_registry_and_native_target( canary.assert_not_called() +@pytest.mark.usefixtures("clear_find_binary_cache") +@pytest.mark.agent_authored(model="gpt-5.6") +def test_find_binary_windows_ncu_launcher_hit_does_not_resolve_machine_arch(monkeypatch, mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") + launcher = os.path.join(install_root, "ncu.bat") + + mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) + mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) + monkeypatch.delenv("CONDA_PREFIX", raising=False) + registry_root = mocker.patch.object( + binary_finder_module.windows_nsight, "_installed_product_root", return_value=install_root + ) + machine_arch = mocker.patch.object(binary_finder_module.windows_nsight, "windows_machine_arch") + get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") + canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") + checked = _patch_exec_probe(mocker, existing=[launcher]) + + assert find_nvidia_binary_utility("ncu") == os.path.abspath(launcher) + assert checked == [launcher] + registry_root.assert_called_once_with("Compute") + machine_arch.assert_not_called() + get_cuda_path.assert_not_called() + canary.assert_not_called() + + @pytest.mark.parametrize(("utility_name", "product"), (("nsys", "Systems"), ("ncu", "Compute"))) @pytest.mark.usefixtures("clear_find_binary_cache") @pytest.mark.agent_authored(model="gpt-5.6") @@ -355,8 +306,10 @@ def test_find_binary_windows_nsight_registry_miss_is_terminal(monkeypatch, mocke mocker.patch.object(binary_finder_module, "IS_WINDOWS", new=True) mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) monkeypatch.delenv("CONDA_PREFIX", raising=False) - registry_root = mocker.patch.object(binary_finder_module, "_windows_installed_nsight_root", return_value=None) - machine_arch = mocker.patch.object(binary_finder_module, "windows_machine_arch") + registry_root = mocker.patch.object( + binary_finder_module.windows_nsight, "_installed_product_root", return_value=None + ) + machine_arch = mocker.patch.object(binary_finder_module.windows_nsight, "windows_machine_arch") get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") @@ -386,16 +339,16 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_early_search(monkeyp find_sub_dirs = mocker.patch.object(binary_finder_module, "find_sub_dirs_all_sitepackages", return_value=[site_dir]) monkeypatch.setenv("CONDA_PREFIX", conda_prefix) get_cuda_path = mocker.patch.object(binary_finder_module, "get_cuda_path_or_home") - nsys_finder = mocker.patch.object(binary_finder_module, "_find_windows_nsys") - ncu_finder = mocker.patch.object(binary_finder_module, "_find_windows_ncu") + nsys_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "nsys_candidate_paths") + ncu_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "ncu_candidate_paths") checked = _patch_exec_probe(mocker, existing=[expected]) assert find_nvidia_binary_utility(utility_name) == os.path.abspath(expected) assert checked == [os.path.join(site_dir, f"{utility_name}.exe"), expected] find_sub_dirs.assert_called_once_with(site_key.split(os.sep)) get_cuda_path.assert_not_called() - nsys_finder.assert_not_called() - ncu_finder.assert_not_called() + nsys_candidates.assert_not_called() + ncu_candidates.assert_not_called() @pytest.mark.parametrize("utility_name", ("nsight-sys", "nsight-compute")) @@ -409,8 +362,8 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_ctk(monkeypatch, moc mocker.patch.object(binary_finder_module.supported_nvidia_binaries, "SITE_PACKAGES_BINDIRS", {}) monkeypatch.delenv("CONDA_PREFIX", raising=False) mocker.patch.object(binary_finder_module, "get_cuda_path_or_home", return_value=cuda_home) - nsys_finder = mocker.patch.object(binary_finder_module, "_find_windows_nsys") - ncu_finder = mocker.patch.object(binary_finder_module, "_find_windows_ncu") + nsys_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "nsys_candidate_paths") + ncu_candidates = mocker.patch.object(binary_finder_module.windows_nsight, "ncu_candidate_paths") canary = mocker.patch.object(binary_finder_module, "_resolve_ctk_root_via_canary") checked = _patch_exec_probe(mocker, existing=[expected]) @@ -420,132 +373,11 @@ def test_find_windows_nsight_legacy_names_remain_literal_in_ctk(monkeypatch, moc os.path.join(cuda_home, "bin", "x86_64", f"{utility_name}.exe"), expected, ] - nsys_finder.assert_not_called() - ncu_finder.assert_not_called() + nsys_candidates.assert_not_called() + ncu_candidates.assert_not_called() canary.assert_not_called() -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_reads_64_bit_registry(mocker): - install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") - product_key = mocker.MagicMock() - version_key = mocker.MagicMock() - product_context = mocker.MagicMock() - product_context.__enter__.return_value = product_key - version_context = mocker.MagicMock() - version_context.__enter__.return_value = version_key - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = (product_context, version_context) - winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) - - assert binary_finder_module._windows_installed_nsight_root("Systems") == install_root - access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY - winreg.OpenKey.assert_has_calls( - ( - mocker.call( - winreg.HKEY_LOCAL_MACHINE, - rf"{binary_finder_module._NSIGHT_REGISTRY_ROOT}\Systems", - 0, - access, - ), - mocker.call(product_key, "2026.1.3", 0, access), - ) - ) - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_returns_none_when_product_key_is_absent(mocker): - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = FileNotFoundError("Nsight Systems is not installed") - - assert binary_finder_module._windows_installed_nsight_root("Systems") is None - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_rejects_missing_current_version(mocker): - product_context = mocker.MagicMock() - product_context.__enter__.return_value = mocker.MagicMock() - winreg = _patch_winreg(mocker) - winreg.OpenKey.return_value = product_context - winreg.QueryValueEx.side_effect = FileNotFoundError("CurrentVersion is missing") - - with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: - binary_finder_module._windows_installed_nsight_root("Systems") - - assert isinstance(exc_info.value.__cause__, FileNotFoundError) - - -@pytest.mark.parametrize("current_version", (None, "", " ", 2026)) -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_rejects_invalid_current_version(mocker, current_version): - product_context = mocker.MagicMock() - product_context.__enter__.return_value = mocker.MagicMock() - winreg = _patch_winreg(mocker) - winreg.OpenKey.return_value = product_context - winreg.QueryValueEx.return_value = (current_version, 1) - - with pytest.raises(RuntimeError, match=r"Invalid CurrentVersion value .*Nsight 'Systems' registry registration"): - binary_finder_module._windows_installed_nsight_root("Systems") - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_rejects_missing_version_key(mocker): - product_key = mocker.MagicMock() - product_context = mocker.MagicMock() - product_context.__enter__.return_value = product_key - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = (product_context, FileNotFoundError("Version key is missing")) - winreg.QueryValueEx.return_value = ("2026.1.3", 1) - - with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: - binary_finder_module._windows_installed_nsight_root("Systems") - - assert isinstance(exc_info.value.__cause__, FileNotFoundError) - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_rejects_missing_installation_directory(mocker): - product_context = mocker.MagicMock() - product_context.__enter__.return_value = mocker.MagicMock() - version_context = mocker.MagicMock() - version_context.__enter__.return_value = mocker.MagicMock() - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = (product_context, version_context) - winreg.QueryValueEx.side_effect = (("2026.1.3", 1), FileNotFoundError("Installation directory is missing")) - - with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: - binary_finder_module._windows_installed_nsight_root("Systems") - - assert isinstance(exc_info.value.__cause__, FileNotFoundError) - - -@pytest.mark.parametrize("install_root", (None, "", " ", 2026)) -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_rejects_invalid_installation_directory(mocker, install_root): - product_context = mocker.MagicMock() - product_context.__enter__.return_value = mocker.MagicMock() - version_context = mocker.MagicMock() - version_context.__enter__.return_value = mocker.MagicMock() - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = (product_context, version_context) - winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) - - with pytest.raises( - RuntimeError, - match=r"Invalid installation directory .*Nsight 'Systems' registry registration.*version '2026.1.3'", - ): - binary_finder_module._windows_installed_nsight_root("Systems") - - -@pytest.mark.agent_authored(model="gpt-5.6") -def test_windows_installed_nsight_root_propagates_access_errors(mocker): - winreg = _patch_winreg(mocker) - winreg.OpenKey.side_effect = PermissionError("Registry access denied") - - with pytest.raises(PermissionError, match="Registry access denied"): - binary_finder_module._windows_installed_nsight_root("Systems") - - @pytest.mark.usefixtures("clear_find_binary_cache") def test_find_binary_first_matching_dir_wins(monkeypatch, mocker): conda_prefix = os.path.join(os.sep, "conda") diff --git a/cuda_pathfinder/tests/test_windows_nsight.py b/cuda_pathfinder/tests/test_windows_nsight.py new file mode 100644 index 00000000000..78da205988b --- /dev/null +++ b/cuda_pathfinder/tests/test_windows_nsight.py @@ -0,0 +1,196 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +import os + +import pytest + +from cuda.pathfinder._binaries import windows_nsight + + +def _patch_winreg(mocker): + winreg = mocker.MagicMock() + winreg.HKEY_LOCAL_MACHINE = object() + winreg.KEY_READ = 0x20019 + winreg.KEY_WOW64_64KEY = 0x0100 + mocker.patch.object(windows_nsight.importlib, "import_module", return_value=winreg) + return winreg + + +@pytest.mark.parametrize( + ("machine_arch", "target_dir"), + ( + ("x64", "target-windows-x64"), + ("arm64", "target-windows-armv8"), + ), +) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_nsys_candidate_paths_use_machine_arch(mocker, machine_arch, target_dir): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + expected = os.path.join(install_root, target_dir, "nsys.exe") + mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root) + mocker.patch.object(windows_nsight, "windows_machine_arch", return_value=machine_arch) + + assert tuple(windows_nsight.nsys_candidate_paths()) == (expected,) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_nsys_candidate_paths_do_not_include_other_arch(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + arm64 = os.path.join(install_root, "target-windows-armv8", "nsys.exe") + mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root) + mocker.patch.object(windows_nsight, "windows_machine_arch", return_value="arm64") + + assert tuple(windows_nsight.nsys_candidate_paths()) == (arm64,) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_ncu_candidate_paths_yield_launcher_before_resolving_machine_arch(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") + launcher = os.path.join(install_root, "ncu.bat") + mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root) + machine_arch = mocker.patch.object(windows_nsight, "windows_machine_arch") + + candidates = windows_nsight.ncu_candidate_paths() + + assert next(candidates) == launcher + machine_arch.assert_not_called() + + +@pytest.mark.parametrize( + ("machine_arch", "target_dir"), + ( + ("x64", os.path.join("target", "windows-desktop-win7-x64")), + ("arm64", os.path.join("target", "windows-desktop-win10-t23x-a64")), + ), +) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_ncu_candidate_paths_fall_back_to_machine_binary(mocker, machine_arch, target_dir): + install_root = os.path.join(os.sep, "Program Files", "Nsight Compute") + launcher = os.path.join(install_root, "ncu.bat") + expected = os.path.join(install_root, target_dir, "ncu.exe") + mocker.patch.object(windows_nsight, "_installed_product_root", return_value=install_root) + mocker.patch.object(windows_nsight, "windows_machine_arch", return_value=machine_arch) + + assert tuple(windows_nsight.ncu_candidate_paths()) == (launcher, expected) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_reads_64_bit_registry(mocker): + install_root = os.path.join(os.sep, "Program Files", "Nsight Systems") + product_key = mocker.MagicMock() + version_key = mocker.MagicMock() + product_context = mocker.MagicMock() + product_context.__enter__.return_value = product_key + version_context = mocker.MagicMock() + version_context.__enter__.return_value = version_key + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) + + assert windows_nsight._installed_product_root("Systems") == install_root + access = winreg.KEY_READ | winreg.KEY_WOW64_64KEY + winreg.OpenKey.assert_has_calls( + ( + mocker.call( + winreg.HKEY_LOCAL_MACHINE, + rf"{windows_nsight._REGISTRY_ROOT}\Systems", + 0, + access, + ), + mocker.call(product_key, "2026.1.3", 0, access), + ) + ) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_returns_none_when_product_key_is_absent(mocker): + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = FileNotFoundError("Nsight Systems is not installed") + + assert windows_nsight._installed_product_root("Systems") is None + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_rejects_missing_current_version(mocker): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.return_value = product_context + winreg.QueryValueEx.side_effect = FileNotFoundError("CurrentVersion is missing") + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + windows_nsight._installed_product_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.parametrize("current_version", (None, "", " ", 2026)) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_rejects_invalid_current_version(mocker, current_version): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.return_value = product_context + winreg.QueryValueEx.return_value = (current_version, 1) + + with pytest.raises(RuntimeError, match=r"Invalid CurrentVersion value .*Nsight 'Systems' registry registration"): + windows_nsight._installed_product_root("Systems") + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_rejects_missing_version_key(mocker): + product_key = mocker.MagicMock() + product_context = mocker.MagicMock() + product_context.__enter__.return_value = product_key + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, FileNotFoundError("Version key is missing")) + winreg.QueryValueEx.return_value = ("2026.1.3", 1) + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + windows_nsight._installed_product_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_rejects_missing_installation_directory(mocker): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + version_context = mocker.MagicMock() + version_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), FileNotFoundError("Installation directory is missing")) + + with pytest.raises(RuntimeError, match=r"Incomplete Nsight 'Systems' registry registration") as exc_info: + windows_nsight._installed_product_root("Systems") + + assert isinstance(exc_info.value.__cause__, FileNotFoundError) + + +@pytest.mark.parametrize("install_root", (None, "", " ", 2026)) +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_rejects_invalid_installation_directory(mocker, install_root): + product_context = mocker.MagicMock() + product_context.__enter__.return_value = mocker.MagicMock() + version_context = mocker.MagicMock() + version_context.__enter__.return_value = mocker.MagicMock() + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = (product_context, version_context) + winreg.QueryValueEx.side_effect = (("2026.1.3", 1), (install_root, 1)) + + with pytest.raises( + RuntimeError, + match=r"Invalid installation directory .*Nsight 'Systems' registry registration.*version '2026.1.3'", + ): + windows_nsight._installed_product_root("Systems") + + +@pytest.mark.agent_authored(model="gpt-5.6") +def test_installed_product_root_propagates_access_errors(mocker): + winreg = _patch_winreg(mocker) + winreg.OpenKey.side_effect = PermissionError("Registry access denied") + + with pytest.raises(PermissionError, match="Registry access denied"): + windows_nsight._installed_product_root("Systems")