# Copyright (c) Advanced Micro Devices, Inc. # SPDX-License-Identifier: MIT """Unit tests for utils.inject_roctx._backends.torch_cpp_loader. No GPU. CMake build is end-to-end in test_torch_trace_coverage; this file stubs the loader to pin contracts: tier order, cache fingerprint, REBUILD env var, no-ninja policy. """ import importlib import sys import common # noqa: F401 import pytest from utils.inject_roctx._backends import torch_cpp_loader as inject_roctx_loader _FAKE_TAG = "py3.12_torch2.9_src000000000000" # --------------------------------------------------------------------------- # compute_tag / fingerprint # --------------------------------------------------------------------------- def test_compute_tag_returns_well_formed_string(): """The cache tag follows ``py._torch_src<12-hex>``.""" tag = inject_roctx_loader.compute_tag() if tag is None: pytest.skip("torch not importable") parts = tag.split("_") assert any(p.startswith("py") for p in parts) assert any(p.startswith("torch") for p in parts) src_components = [p for p in parts if p.startswith("src")] assert len(src_components) == 1, ( f"expected exactly one '_src...' component in tag {tag!r}" ) src_value = src_components[0][len("src") :] assert src_value == "missing" or ( len(src_value) == 12 and all(c in "0123456789abcdef" for c in src_value) ), f"unexpected src component {src_value!r} in tag {tag!r}" def test_compute_tag_is_stable_across_calls(): """``compute_tag()`` returns the same value for repeated calls in one process.""" assert inject_roctx_loader.compute_tag() == inject_roctx_loader.compute_tag() def test_source_fingerprint_changes_when_inputs_change(tmp_path, monkeypatch): """A single-byte edit to any input changes the fingerprint.""" cpp = tmp_path / "roctx_recordfn.cpp" cmake = tmp_path / "CMakeLists.txt" cpp.write_text("// fingerprint test source\n") cmake.write_text("# fingerprint test buildfile\n") monkeypatch.setattr( inject_roctx_loader, "_FINGERPRINT_INPUTS", (cpp, cmake), ) baseline = inject_roctx_loader._source_fingerprint() assert len(baseline) == 12 for input_path in (cpp, cmake): original = input_path.read_bytes() input_path.write_bytes(original + b"\n# mutation\n") mutated = inject_roctx_loader._source_fingerprint() assert mutated != baseline, ( f"editing {input_path.name} did not change the fingerprint" ) input_path.write_bytes(original) assert inject_roctx_loader._source_fingerprint() == baseline def test_source_fingerprint_excludes_tool_version_file(): """The tool ``VERSION`` file is not a fingerprint input.""" for path in inject_roctx_loader._FINGERPRINT_INPUTS: assert path.name != "VERSION", ( f"VERSION should not be in _FINGERPRINT_INPUTS; saw {path}" ) def test_source_fingerprint_is_missing_sentinel_when_no_inputs_readable( tmp_path, monkeypatch, ): """When no inputs are readable, the fingerprint is the ``"missing"`` sentinel.""" monkeypatch.setattr( inject_roctx_loader, "_FINGERPRINT_INPUTS", (tmp_path / "does_not_exist.cpp",), ) assert inject_roctx_loader._source_fingerprint() == "missing" def test_source_fingerprint_is_length_delimited(tmp_path, monkeypatch): """Reshuffling bytes across input boundaries changes the fingerprint.""" a = tmp_path / "a.cpp" b = tmp_path / "b.txt" monkeypatch.setattr(inject_roctx_loader, "_FINGERPRINT_INPUTS", (a, b)) a.write_bytes(b"AB") b.write_bytes(b"C") fp1 = inject_roctx_loader._source_fingerprint() a.write_bytes(b"A") b.write_bytes(b"BC") fp2 = inject_roctx_loader._source_fingerprint() assert fp1 != fp2, "fingerprint collided across an input boundary" def test_cmake_and_runtime_compute_identical_fingerprint(): """The loader and CMake compute the same source fingerprint.""" import subprocess cmake_dir = inject_roctx_loader._SO_SOURCE_DIR assert cmake_dir.is_dir(), f"missing cmake source dir at {cmake_dir}" snippet = ( "import sys, pathlib; " f"sys.path.insert(0, str(pathlib.Path('{cmake_dir}/../..').resolve())); " "from utils.inject_roctx._backends.torch_cpp_loader " "import _source_fingerprint; " "print(_source_fingerprint())" ) result = subprocess.run( [sys.executable, "-c", snippet], check=True, capture_output=True, text=True, ) cmake_side = result.stdout.strip() runtime_side = inject_roctx_loader._source_fingerprint() assert cmake_side == runtime_side, ( f"install-time fingerprint {cmake_side!r} != runtime {runtime_side!r}" ) # --------------------------------------------------------------------------- # Source / buildfile hygiene # --------------------------------------------------------------------------- def test_roctx_recordfn_source_avoids_torch_umbrella_headers(): """The source must not include ````.""" cpp_path = inject_roctx_loader._SO_SOURCE assert cpp_path.is_file(), f"missing C++ source at {cpp_path}" active_lines = [ line for line in cpp_path.read_text().splitlines() if not line.lstrip().startswith("//") ] active_src = "\n".join(active_lines) forbidden = ( "", "", "", ) for header in forbidden: directive = f"#include {header}" assert directive not in active_src, f"must not include {header}" def test_roctx_recordfn_source_uses_narrow_includes(): """The source includes only the required narrow PyTorch headers.""" cpp_path = inject_roctx_loader._SO_SOURCE src = cpp_path.read_text() required = ( "#include ", "#include ", "#include ", "#include ", ) for directive in required: assert directive in src, f"must include {directive}" def test_cmake_buildfile_does_not_override_output_name(): """``CMakeLists.txt`` must not set ``OUTPUT_NAME``.""" cmake_path = inject_roctx_loader._SO_BUILDFILE assert cmake_path.is_file(), f"missing CMakeLists.txt at {cmake_path}" active_lines = [ line for line in cmake_path.read_text().splitlines() if not line.lstrip().startswith("#") ] active_src = "\n".join(active_lines) assert "OUTPUT_NAME" not in active_src, "must not set OUTPUT_NAME" def test_cmake_buildfile_strips_lib_prefix(): """``CMakeLists.txt`` sets ``PREFIX ""`` so the artifact is not ``lib``-prefixed.""" import re cmake_path = inject_roctx_loader._SO_BUILDFILE active_lines = [ line for line in cmake_path.read_text().splitlines() if not line.lstrip().startswith("#") ] active_src = "\n".join(active_lines) assert re.search(r'PREFIX\s+""', active_src), 'must set PREFIX ""' def test_loader_and_cmake_agree_on_artifact_filename_shape(): """The loader and CMake produce identical artifact filenames.""" import inspect src = inspect.getsource(inject_roctx_loader._try_jit) assert 'f"roctx_recordfn-{tag}.so"' in src, ( "artifact filename has changed; update this test accordingly" ) def test_loader_source_never_recommends_installing_ninja(): """The loader source must not recommend installing ninja.""" import inspect as _stdlib_inspect src = _stdlib_inspect.getsource(inject_roctx_loader).lower() forbidden = ( "pip install ninja", "apt install ninja", "apt-get install ninja", "install ninja", "add ninja to requirements", "ninja>=", "ninja==", ) for token in forbidden: assert token not in src, f"loader source must not contain {token!r}" # --------------------------------------------------------------------------- # Tier-name surface # --------------------------------------------------------------------------- def test_c_tier_names_matches_the_tier_ladder(): """``C_TIER_NAMES`` enumerates exactly the prebuilt and JIT tiers.""" assert inject_roctx_loader.C_TIER_NAMES == frozenset(( inject_roctx_loader.TIER_PREBUILT, inject_roctx_loader.TIER_JIT, )) # --------------------------------------------------------------------------- # Misc helpers # --------------------------------------------------------------------------- def test_force_python_fallback_returns_none(): assert inject_roctx_loader.load(force_python_fallback=True).module is None def test_install_cached_so_overwrites_stale_artifact(tmp_path): """``_install_cached_so`` overwrites an existing cached ``.so``.""" src = tmp_path / "build" / "roctx_recordfn.so" src.parent.mkdir() src.write_bytes(b"new content") cached = tmp_path / "cache" / "roctx_recordfn-tag.so" cached.parent.mkdir() cached.write_bytes(b"STALE content -- must be overwritten") inject_roctx_loader._install_cached_so(src, cached) assert cached.read_bytes() == b"new content", ( "cache copy did not overwrite the existing cached .so" ) def test_install_cached_so_is_a_noop_when_src_missing(tmp_path): """``_install_cached_so`` is a no-op when the source ``.so`` is missing.""" src = tmp_path / "build" / "does_not_exist.so" cached = tmp_path / "cache" / "roctx_recordfn-tag.so" cached.parent.mkdir() cached.write_bytes(b"good content") inject_roctx_loader._install_cached_so(src, cached) assert cached.read_bytes() == b"good content", ( "missing src must leave cached .so untouched" ) def test_jit_cache_dir_is_creatable(monkeypatch, tmp_path): """``_jit_cache_dir`` creates the directory on first call and is idempotent.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) d = inject_roctx_loader._jit_cache_dir() assert d.exists() and d.is_dir() assert inject_roctx_loader._jit_cache_dir() == d def test_cmake_executable_honors_env_var_then_falls_back(monkeypatch): """``$CMAKE`` takes precedence over ``PATH``.""" seen = [] def fake_which(name): seen.append(name) return f"/fake/bin/{name}" monkeypatch.setattr(inject_roctx_loader.shutil, "which", fake_which) monkeypatch.setenv("CMAKE", "my-custom-cmake") assert inject_roctx_loader._cmake_executable() == "/fake/bin/my-custom-cmake" assert seen[-1] == "my-custom-cmake" monkeypatch.delenv("CMAKE", raising=False) assert inject_roctx_loader._cmake_executable() == "/fake/bin/cmake" assert seen[-1] == "cmake" def test_no_prebuilt_returns_none_for_unknown_tag(monkeypatch): """``_try_prebuilt`` returns ``None`` when no candidate matches the tag.""" monkeypatch.setattr( inject_roctx_loader, "_install_tree_prebuilt_candidates", lambda _tag: [], ) assert inject_roctx_loader._try_prebuilt("py3.10_torch2.9") is None # --------------------------------------------------------------------------- # JIT failure marker # --------------------------------------------------------------------------- def test_jit_failure_marker_round_trip(monkeypatch, tmp_path): """``_previous_jit_failure`` reads the reason from ``_record_jit_failure``.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) tag = _FAKE_TAG assert inject_roctx_loader._previous_jit_failure(tag) is None inject_roctx_loader._record_jit_failure(tag, RuntimeError("ninja missing")) recorded = inject_roctx_loader._previous_jit_failure(tag) assert recorded is not None assert "ninja missing" in recorded assert "RuntimeError" in recorded def test_jit_failure_marker_cleared_on_demand(monkeypatch, tmp_path): """_clear_jit_failure() must remove the marker and be idempotent.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) tag = _FAKE_TAG inject_roctx_loader._record_jit_failure(tag, RuntimeError("x")) assert inject_roctx_loader._previous_jit_failure(tag) is not None inject_roctx_loader._clear_jit_failure(tag) assert inject_roctx_loader._previous_jit_failure(tag) is None inject_roctx_loader._clear_jit_failure(tag) # --------------------------------------------------------------------------- # _try_jit: cache-hit path # --------------------------------------------------------------------------- def test_try_jit_returns_cache_hit_without_invoking_cmake(monkeypatch, tmp_path): """An on-disk cached ``.so`` is loaded directly, no cmake invocation.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) cache_dir = inject_roctx_loader._jit_cache_dir() cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so" cached_so.write_bytes(b"stub-so") sentinel = object() monkeypatch.setattr( inject_roctx_loader, "_import_module_from_path", lambda _n, _p: sentinel, ) def fail_subprocess(*_a, **_k): pytest.fail("subprocess.run must not fire on cache hit") monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess) assert inject_roctx_loader._try_jit(_FAKE_TAG) is sentinel def test_try_jit_force_rebuild_bypasses_cache(monkeypatch, tmp_path): """``force_rebuild=True`` ignores the cache and reaches the build path.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) cache_dir = inject_roctx_loader._jit_cache_dir() cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so" cached_so.write_bytes(b"stale") def must_not_import(_n, _p): pytest.fail("cache hit must not be consulted under force_rebuild") monkeypatch.setattr( inject_roctx_loader, "_import_module_from_path", must_not_import ) monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", tmp_path / "missing.cpp") assert inject_roctx_loader._try_jit(_FAKE_TAG, force_rebuild=True) is None # --------------------------------------------------------------------------- # _try_jit: cmake build path # --------------------------------------------------------------------------- class _StubCompleted: """Minimal ``subprocess.CompletedProcess`` stand-in.""" def __init__(self, returncode=0, stdout="", stderr=""): self.returncode = returncode self.stdout = stdout self.stderr = stderr def _set_so_inputs_present(monkeypatch, tmp_path): """Point the loader at synthetic source files.""" src_dir = tmp_path / "roctx_recordfn" src_dir.mkdir(parents=True, exist_ok=True) cpp = src_dir / "roctx_recordfn.cpp" cml = src_dir / "CMakeLists.txt" cpp.write_text("// stub\n") cml.write_text("# stub\n") monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE_DIR", src_dir) monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", cpp) monkeypatch.setattr(inject_roctx_loader, "_SO_BUILDFILE", cml) return src_dir def test_try_jit_skips_when_sources_missing(monkeypatch, tmp_path): """``_try_jit`` returns ``None`` when sources are missing (no cache hit).""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) monkeypatch.setattr(inject_roctx_loader, "_SO_SOURCE", tmp_path / "nonexistent.cpp") monkeypatch.setattr( inject_roctx_loader, "_SO_BUILDFILE", tmp_path / "nonexistent.txt" ) def fail_subprocess(*_a, **_k): pytest.fail("subprocess.run must not be called when sources are missing") monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None def test_try_jit_skips_when_cmake_not_on_path(monkeypatch, tmp_path): """Absence of cmake on ``PATH`` does not write a failure marker.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: None) def fail_subprocess(*_a, **_k): pytest.fail("subprocess.run must not be called when cmake is absent") monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is None def test_try_jit_short_circuits_on_prior_failure(monkeypatch, tmp_path): """A prior failure marker vetoes ``_try_jit``.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") inject_roctx_loader._record_jit_failure( _FAKE_TAG, RuntimeError("earlier failure"), ) def fail_subprocess(*_a, **_k): pytest.fail("subprocess.run must not fire when marker is present") monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fail_subprocess) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None def test_try_jit_passes_runtime_python_to_cmake(monkeypatch, tmp_path): """``_try_jit`` passes ``-DTORCH_TRACE_PYTHON=sys.executable`` to cmake.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") invocations = [] def fake_run(argv, **_kw): invocations.append(list(argv)) return _StubCompleted(returncode=0) monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run) cache_dir = inject_roctx_loader._jit_cache_dir() build_dir = cache_dir / f"cmake-build-{_FAKE_TAG}" produced = build_dir / f"roctx_recordfn-{_FAKE_TAG}.so" produced.parent.mkdir(parents=True, exist_ok=True) produced.write_bytes(b"stub-so") monkeypatch.setattr( inject_roctx_loader, "_import_module_from_path", lambda _n, _p: object(), ) inject_roctx_loader._try_jit(_FAKE_TAG) assert len(invocations) == 2, f"expected two cmake invocations, saw {invocations!r}" configure_argv = invocations[0] runtime_python_flag = f"-DTORCH_TRACE_PYTHON={sys.executable}" assert runtime_python_flag in configure_argv, ( f"-DTORCH_TRACE_PYTHON must equal sys.executable; saw {configure_argv!r}" ) build_argv = invocations[1] assert build_argv[1] == "--build", ( f"second invocation must be `cmake --build`, saw {build_argv!r}" ) def test_try_jit_records_failure_marker_on_configure_failure(monkeypatch, tmp_path): """A non-zero configure return code writes a JIT failure marker.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") def fake_run(argv, **_kw): return _StubCompleted( returncode=1, stderr="CMake Error: Could not find Torch (missing: TORCH_DIR)\n", ) monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG) assert marker is not None assert inject_roctx_loader.TIER_JIT in marker assert "Could not find Torch" in marker, ( f"marker must preserve stderr tail; saw {marker!r}" ) def test_try_jit_records_failure_marker_on_build_failure(monkeypatch, tmp_path): """A non-zero build return code writes a JIT failure marker.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") call_count = [0] def fake_run(argv, **_kw): call_count[0] += 1 if call_count[0] == 1: return _StubCompleted(returncode=0) return _StubCompleted( returncode=2, stderr="error: undefined reference to `roctxRangePushA'\n", ) monkeypatch.setattr(inject_roctx_loader.subprocess, "run", fake_run) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG) assert marker is not None assert inject_roctx_loader.TIER_JIT in marker assert "undefined reference" in marker def test_try_jit_records_failure_marker_when_so_missing(monkeypatch, tmp_path): """A successful build with no ``.so`` output writes a failure marker.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") monkeypatch.setattr( inject_roctx_loader.subprocess, "run", lambda *a, **k: _StubCompleted(returncode=0), ) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None marker = inject_roctx_loader._previous_jit_failure(_FAKE_TAG) assert marker is not None assert inject_roctx_loader.TIER_JIT in marker assert "missing" in marker def test_try_jit_cleans_build_dir_on_success(monkeypatch, tmp_path): """The build directory is removed after a successful build.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") monkeypatch.setattr( inject_roctx_loader.subprocess, "run", lambda *a, **k: _StubCompleted(returncode=0), ) cache_dir = inject_roctx_loader._jit_cache_dir() build_dir = cache_dir / f"cmake-build-{_FAKE_TAG}" produced = build_dir / f"roctx_recordfn-{_FAKE_TAG}.so" produced.parent.mkdir(parents=True, exist_ok=True) produced.write_bytes(b"stub-so") leftover = build_dir / "CMakeFiles" leftover.mkdir(exist_ok=True) (leftover / "stale.o").write_bytes(b"x") monkeypatch.setattr( inject_roctx_loader, "_import_module_from_path", lambda _n, _p: object(), ) result = inject_roctx_loader._try_jit(_FAKE_TAG) assert result is not None assert not build_dir.exists(), ( "build dir must be removed on success to bound cache disk usage" ) cached_so = cache_dir / f"roctx_recordfn-{_FAKE_TAG}.so" assert cached_so.exists() and cached_so.read_bytes() == b"stub-so", ( "produced .so must have been copied to the cache before cleanup" ) def test_try_jit_keeps_build_dir_on_failure(monkeypatch, tmp_path): """Preserve the build dir on failure for post-mortem inspection.""" monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) _set_so_inputs_present(monkeypatch, tmp_path) monkeypatch.setattr(inject_roctx_loader, "_cmake_executable", lambda: "/fake/cmake") monkeypatch.setattr( inject_roctx_loader.subprocess, "run", lambda *a, **k: _StubCompleted(returncode=1, stderr="boom\n"), ) assert inject_roctx_loader._try_jit(_FAKE_TAG) is None build_dir = inject_roctx_loader._jit_cache_dir() / f"cmake-build-{_FAKE_TAG}" assert build_dir.exists(), "build dir must survive a failure" # --------------------------------------------------------------------------- # explain_cmake_failure classification # --------------------------------------------------------------------------- def test_explain_cmake_failure_classifies_torch_not_found(): reason, hint = inject_roctx_loader._explain_cmake_failure( "configure", RuntimeError("rc=1"), "CMake Error: Could not find Torch (missing: TORCH_DIR)\n", ) assert "libtorch" in reason.lower() assert "torch" in hint.lower() def test_explain_cmake_failure_classifies_roctx_not_found(): reason, hint = inject_roctx_loader._explain_cmake_failure( "configure", RuntimeError("rc=1"), "find_library failed: rocprofiler-sdk-roctx not found\n", ) assert "roctx" in reason.lower() or "rocprofiler-sdk-roctx" in reason.lower() assert "rocm_path" in hint.lower() or "/opt/rocm" in hint.lower() def test_explain_cmake_failure_classifies_missing_cxx_compiler(): reason, hint = inject_roctx_loader._explain_cmake_failure( "configure", RuntimeError("rc=1"), "No CMAKE_CXX_COMPILER could be found.\n", ) assert "compiler" in reason.lower() assert "g++" in hint.lower() or "clang" in hint.lower() def test_explain_cmake_failure_never_recommends_installing_ninja(): """cmake-tier hints must not recommend installing ninja.""" samples = [ (RuntimeError("rc=1"), "CMake Error: ninja not found"), (RuntimeError("rc=1"), "Could not find Torch"), (RuntimeError("rc=1"), ""), ] for err, stderr in samples: reason, hint = inject_roctx_loader._explain_cmake_failure( "configure", err, stderr, ) forbidden = ("install ninja", "pip install ninja", "apt install ninja") joined = (reason + " " + hint).lower() for token in forbidden: assert token not in joined, ( f"cmake-tier hint must not recommend installing ninja " f"(found {token!r} in: {hint!r})" ) # --------------------------------------------------------------------------- # load(): tier ordering and REBUILD override # --------------------------------------------------------------------------- def test_rebuild_env_var_clears_failure_marker(monkeypatch, tmp_path): """``ROCPROFCOMPUTE_REBUILD_ROCTX=1`` clears any cached failure marker.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path)) monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1") inject_roctx_loader._record_jit_failure(_FAKE_TAG, RuntimeError("stale failure")) assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is not None sentinel = object() monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: pytest.fail("prebuilt must be skipped"), ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: sentinel, ) result = inject_roctx_loader.load() assert result.module is sentinel assert inject_roctx_loader._previous_jit_failure(_FAKE_TAG) is None, ( "REBUILD must clear the negative cache before the forced rebuild" ) def test_rebuild_env_var_skips_prebuilt_and_forces_jit_rebuild(monkeypatch): """REBUILD=1 routes directly to the JIT build, skipping prebuilt and cache.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) calls = [] monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda tag, diagnostics=None: calls.append(("prebuilt", tag)) or None, ) sentinel = object() def _jit(tag, force_rebuild=False, diagnostics=None): calls.append(("jit", tag, force_rebuild)) return sentinel monkeypatch.setattr(inject_roctx_loader, "_try_jit", _jit) monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1") result = inject_roctx_loader.load() assert result.module is sentinel assert [c[0] for c in calls] == ["jit"], ( f"expected only _try_jit to fire under REBUILD; saw {calls!r}" ) assert calls[0][1] == _FAKE_TAG, "tag must propagate to the build step" assert calls[0][2] is True, "REBUILD must pass force_rebuild=True" def test_rebuild_env_var_returns_none_when_build_fails(monkeypatch): """Under REBUILD, JIT failing yields ``None`` with no fallback.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: pytest.fail("_try_prebuilt called"), ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: None, ) monkeypatch.setenv(inject_roctx_loader._REBUILD_ENV_VAR, "1") assert inject_roctx_loader.load().module is None def test_default_load_path_still_tries_prebuilt_first(monkeypatch): """The prebuilt tier short-circuits the default load path.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) calls = [] sentinel = object() monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda tag, diagnostics=None: calls.append("prebuilt") or sentinel, ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda tag, force_rebuild=False, diagnostics=None: calls.append("jit") or None, ) assert inject_roctx_loader.load().module is sentinel assert calls == ["prebuilt"], ( f"expected prebuilt to short-circuit before jit; saw {calls!r}" ) def test_default_load_path_walks_prebuilt_then_jit(monkeypatch): """Tiers are tried in order: prebuilt, then jit.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) calls = [] monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda tag, diagnostics=None: calls.append("prebuilt") or None, ) sentinel = object() monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda tag, force_rebuild=False, diagnostics=None: ( calls.append("jit") or sentinel ), ) assert inject_roctx_loader.load().module is sentinel assert calls == ["prebuilt", "jit"], ( f"expected order prebuilt -> jit, saw {calls!r}" ) def test_load_does_not_raise_when_torch_missing(monkeypatch): """``load()`` returns ``None`` when ``torch`` is not importable.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: None) assert inject_roctx_loader.load().module is None def test_load_returns_module_or_none_no_raise(): """``load()`` returns a module exposing the documented API, or ``None``.""" mod = inject_roctx_loader.load().module if mod is not None: for sym in ( "install", "uninstall", "is_installed", "push_user_scope", "pop_user_scope", "dump_stats", ): assert hasattr(mod, sym), f"loaded module is missing {sym}" # --------------------------------------------------------------------------- # LoadResult: tier / diagnostics / format_load_diagnostic_trail # --------------------------------------------------------------------------- def test_load_result_records_successful_tier(monkeypatch): """``LoadResult.tier`` reports the name of the tier that returned a module.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) sentinel = object() monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: sentinel, ) result = inject_roctx_loader.load() assert result.module is sentinel assert result.tier == inject_roctx_loader.TIER_JIT assert inject_roctx_loader.TIER_JIT in inject_roctx_loader.C_TIER_NAMES def test_load_result_tier_is_none_when_all_tiers_miss(monkeypatch): """``LoadResult.tier`` is ``None`` when every tier returns ``None``.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: None, ) result = inject_roctx_loader.load() assert result.module is None assert result.tier is None def test_load_returns_independent_diagnostics(monkeypatch): """Each ``load()`` call returns its own diagnostics list.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: object() ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: None, ) first = inject_roctx_loader.load().diagnostics second = inject_roctx_loader.load().diagnostics assert first is not second, "each load() must return its own diagnostics list" def test_safe_log_appends_to_provided_diagnostics(): """Each ``_safe_log`` call appends to the provided diagnostics list.""" diagnostics: list[tuple[str, str]] = [] inject_roctx_loader._safe_log("log", "tier A skipped", diagnostics) inject_roctx_loader._safe_log("warning", "tier B failed", diagnostics) inject_roctx_loader._safe_log("log", "final fallback engaged", diagnostics) assert [lvl for lvl, _ in diagnostics] == ["log", "warning", "log"] assert [msg for _, msg in diagnostics] == [ "tier A skipped", "tier B failed", "final fallback engaged", ] def test_load_result_carries_tier_and_diagnostics(monkeypatch): """``load()`` returns the tier scalar and a diagnostics list.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: object(), ) result = inject_roctx_loader.load() assert result.tier == inject_roctx_loader.TIER_JIT assert isinstance(result.diagnostics, list) def test_load_result_returns_python_tier_failure_trail(monkeypatch): """When every tier misses the trail includes the terminal fallback line.""" monkeypatch.setattr(inject_roctx_loader, "compute_tag", lambda: _FAKE_TAG) monkeypatch.delenv(inject_roctx_loader._REBUILD_ENV_VAR, raising=False) monkeypatch.setattr( inject_roctx_loader, "_try_prebuilt", lambda _t, diagnostics=None: None ) monkeypatch.setattr( inject_roctx_loader, "_try_jit", lambda _t, force_rebuild=False, diagnostics=None: None, ) result = inject_roctx_loader.load() assert result.tier is None joined = " ".join(msg for _, msg in result.diagnostics).lower() assert "python-only injector" in joined or "no roctx_recordfn" in joined, ( f"terminal-fallback line missing from trail: " f"{[(lvl, msg) for lvl, msg in result.diagnostics]!r}" ) def test_format_load_diagnostic_trail_handles_empty(): """An empty trail renders to an empty string.""" assert inject_roctx_loader.format_load_diagnostic_trail([]) == "" def test_format_load_diagnostic_trail_caps_lines(): """``format_load_diagnostic_trail`` caps output and keeps the latest lines.""" trail = [("log", f"line {i}") for i in range(100)] rendered = inject_roctx_loader.format_load_diagnostic_trail( trail, max_lines=12, ) lines = rendered.splitlines() assert len(lines) == 12, ( f"expected max_lines=12 to cap output, saw {len(lines)} lines" ) assert "line 99" in rendered, "must keep the trailing (latest) lines" assert "line 0" not in rendered, "must drop the leading (oldest) lines" def test_format_load_diagnostic_trail_includes_level_per_line(): """Each rendered line carries its level (INFO vs WARNING).""" trail = [ ("log", "skipped tier A"), ("warning", "tier B failed"), ] rendered = inject_roctx_loader.format_load_diagnostic_trail(trail) assert "[log]" in rendered assert "[warning]" in rendered assert "skipped tier A" in rendered assert "tier B failed" in rendered # --------------------------------------------------------------------------- # Python fallback integration # --------------------------------------------------------------------------- def test_python_fallback_path_still_works_without_so(monkeypatch): """With the loader returning ``None``, the Python fallback still works.""" try: import torch # noqa: F401 except ImportError: pytest.skip("torch not importable") monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None) if "utils.inject_roctx" in sys.modules: del sys.modules["utils.inject_roctx"] importlib.import_module("utils.inject_roctx") try: from utils.inject_roctx import core from utils.inject_roctx._backends import torch as _torch_backend assert _torch_backend.using_c_tier() is False assert _torch_backend.dump_recordfn_stats() is None pushed: list[str] = [] original_io = core.get_python_tier_io() core.set_python_tier_io(push=pushed.append, pop=lambda: None) try: core._push_scope("py.tier.test", "#1@test:1") core._pop_scope() finally: core.set_python_tier_io(*original_io) assert pushed, "Python-tier rangePush was not invoked in fallback mode" finally: sys.modules.pop("utils.inject_roctx", None) def test_python_fallback_uses_python_dispatch_sentinel(monkeypatch): """When no user frame is available the fallback emits ``python.dispatch:0``.""" try: import torch # noqa: F401 except ImportError: pytest.skip("torch not importable; utils.inject_roctx module-load exits") monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None) if "utils.inject_roctx" in sys.modules: del sys.modules["utils.inject_roctx"] importlib.import_module("utils.inject_roctx") try: from utils.inject_roctx import core monkeypatch.setattr("inspect.currentframe", lambda: None) assert core.resolve_user_caller_location() == "python.dispatch:0" import inspect as _stdlib_inspect src = _stdlib_inspect.getsource(core) assert "dispatcher:0" not in src, "legacy 'dispatcher:0' sentinel still present" finally: sys.modules.pop("utils.inject_roctx", None) def test_import_does_not_apply_global_patches(monkeypatch): """Importing ``utils.inject_roctx`` does not patch PyTorch.""" monkeypatch.setattr(inject_roctx_loader, "load", lambda **kw: None) if "utils.inject_roctx" in sys.modules: del sys.modules["utils.inject_roctx"] try: import torch # noqa: F401 except Exception: pytest.skip("torch not importable") import torch as _torch pre = { "compile": getattr(_torch, "compile", None), } importlib.import_module("utils.inject_roctx") try: post = { "compile": getattr(_torch, "compile", None), } from utils.inject_roctx import core for sym in ( "install_global_wraps", "_push_scope", "_pop_scope", "resolve_user_caller_location", ): assert hasattr(core, sym), f"core symbol missing: {sym}" from utils.inject_roctx._backends import torch as _torch_backend for sym in ( "install_function_apply_wrappers", "using_c_tier", "dump_recordfn_stats", ): assert hasattr(_torch_backend, sym), f"torch backend symbol missing: {sym}" assert post["compile"] is pre["compile"], "torch.compile was replaced on import" finally: sys.modules.pop("utils.inject_roctx", None)