Skip to content

Commit 9adf483

Browse files
Gate checkout-only tests with a root fixture (isaac-sim#7389)
# Description A handful of unit tests inspect repository artifacts that only exist in a source checkout (`apps`, `scripts`, the root `pyproject.toml`, `uv.lock`, `tools/wheel_builder`, and the workflow files). Run from an installed package they fail because those paths are absent. This gates them behind a shared `source_checkout_root` session fixture that locates the checkout and skips the test when it is not present, and returns the root path so the tests stop recomputing it by hand. The fixture lives in `source/isaaclab/test/conftest.py`. The affected tests now take `source_checkout_root` and drop their local `_repo_root()` helpers, so the gating is explicit at the test and the requirement is hard to forget: a checkout-only test needs the root anyway, so it asks for the fixture and gets the skip for free. ## Type of change - Bug fix (non-breaking change which fixes an issue) ## Release backport - [x] <!-- backport-active-release --> Backport this pull request to the active release branch after it merges into `develop` ## Checklist - [x] I have read and understood the contribution guidelines - [x] I have run the `pre-commit` checks with `./isaaclab.sh --format` - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] I have added a changelog fragment under `source/<pkg>/changelog.d/` for every touched package - [ ] I have added my name to the `CONTRIBUTORS.md` or my name already exists there
1 parent 768c305 commit 9adf483

9 files changed

Lines changed: 117 additions & 110 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Test-only: added a source checkout root fixture for tests that inspect repository artifacts.

source/isaaclab/test/app/test_experience_files.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,17 @@
1212

1313
pytestmark = pytest.mark.unit
1414

15-
APPS_DIR = Path(__file__).resolve().parents[4] / "apps"
16-
1715
# ``.kit`` files repeat section headers, so they cannot be parsed as TOML.
1816
_SECTION_RE = re.compile(r"^\s*\[+(?P<name>[^\[\]]+)\]+\s*$")
1917
_DEPENDENCY_RE = re.compile(r'^\s*"(?P<name>[^"]+)"\s*=')
2018

2119

22-
def _kit_dependencies(experience: str) -> set[str]:
20+
def _kit_dependencies(apps_dir: Path, experience: str) -> set[str]:
2321
"""Collect the extension names declared in an experience file's dependency sections."""
2422
dependencies: set[str] = set()
2523
in_dependencies = False
2624

27-
for line in (APPS_DIR / experience).read_text(encoding="utf-8").splitlines():
25+
for line in (apps_dir / experience).read_text(encoding="utf-8").splitlines():
2826
section = _SECTION_RE.match(line)
2927
if section is not None:
3028
in_dependencies = section.group("name").strip() == "dependencies"
@@ -38,9 +36,9 @@ def _kit_dependencies(experience: str) -> set[str]:
3836

3937

4038
@pytest.mark.parametrize("experience", ["isaaclab.python.kit", "isaaclab.python.headless.kit"])
41-
def test_base_experiences_enable_native_storage(experience: str):
39+
def test_base_experiences_enable_native_storage(source_checkout_root: Path, experience: str):
4240
"""Test the base experiences load the extension that applies ``ISAACSIM_ASSET_ROOT``."""
43-
assert "isaacsim.storage.native" in _kit_dependencies(experience)
41+
assert "isaacsim.storage.native" in _kit_dependencies(source_checkout_root / "apps", experience)
4442

4543

4644
@pytest.mark.parametrize(
@@ -52,6 +50,6 @@ def test_base_experiences_enable_native_storage(experience: str):
5250
("isaaclab.python.xr.openxr.headless.kit", "isaaclab.python.xr.openxr"),
5351
],
5452
)
55-
def test_derived_experiences_inherit_native_storage(experience: str, base: str):
53+
def test_derived_experiences_inherit_native_storage(source_checkout_root: Path, experience: str, base: str):
5654
"""Test the derived experiences inherit native storage from a base experience."""
57-
assert base in _kit_dependencies(experience)
55+
assert base in _kit_dependencies(source_checkout_root / "apps", experience)

source/isaaclab/test/cli/test_install.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -476,11 +476,12 @@ class TestPinkIkStack:
476476
stack from there instead of mirroring the versions.
477477
"""
478478

479-
def test_stack_derived_from_root_pyproject_pins(self):
479+
def test_stack_derived_from_root_pyproject_pins(self, source_checkout_root: Path):
480480
"""The derived stack covers every stack package, exactly pinned, markers stripped."""
481481
from isaaclab.cli.commands import install
482482

483-
stack = install._pink_ik_stack()
483+
with mock.patch.object(install, "ISAACLAB_ROOT", source_checkout_root):
484+
stack = install._pink_ik_stack()
484485
assert [install._requirement_name(r) for r in stack] == list(install._PINK_IK_PACKAGES)
485486
assert any(r.startswith("pin-pink==") for r in stack), "pin-pink must stay exactly pinned"
486487
assert any(r.startswith("daqp==") for r in stack), "daqp must stay exactly pinned"

source/isaaclab/test/cli/test_source_package_metadata.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,14 @@
1515
pytestmark = pytest.mark.unit
1616

1717

18-
def _repo_root() -> Path:
19-
"""Find the Isaac Lab repository root from this test file."""
20-
for parent in Path(__file__).resolve().parents:
21-
if (parent / "pyproject.toml").is_file() and (parent / "source").is_dir():
22-
return parent
23-
raise RuntimeError("Could not find Isaac Lab repository root.")
24-
25-
26-
def test_isaaclab_uses_one_standalone_usd_provider():
18+
def test_isaaclab_uses_one_standalone_usd_provider(source_checkout_root: Path):
2719
"""Isaac Lab must install only the USD provider shared with its importer dependencies.
2820
2921
``usd-core`` and ``usd-exchange`` each install a complete ``pxr`` into the same directory, so
3022
a second provider silently overwrites the first and removing either one leaves ``pxr`` broken.
3123
Nothing detects that, because the two are separate distributions.
3224
"""
33-
with (_repo_root() / "pyproject.toml").open("rb") as f:
25+
with (source_checkout_root / "pyproject.toml").open("rb") as f:
3426
pyproject = tomllib.load(f)
3527

3628
dependencies = pyproject["project"]["dependencies"]
@@ -43,14 +35,14 @@ def test_isaaclab_uses_one_standalone_usd_provider():
4335
assert usd_providers == ["usd-exchange==2.3.0"]
4436

4537

46-
def test_resolved_environment_has_no_second_usd_provider():
38+
def test_resolved_environment_has_no_second_usd_provider(source_checkout_root: Path):
4739
"""No dependency may pull ``usd-core`` back in behind an extra.
4840
4941
``newton[importers]``, ``mujoco[usd]`` and ``warp-lang[examples]`` all require it, so selecting
5042
any of them would reinstate the overlap that the direct dependencies avoid. Checking the lock
5143
catches that, where checking ``pyproject.toml`` alone would not.
5244
"""
53-
with (_repo_root() / "uv.lock").open("rb") as f:
45+
with (source_checkout_root / "uv.lock").open("rb") as f:
5446
lock = tomllib.load(f)
5547

5648
locked = {package["name"] for package in lock["package"]}
@@ -59,9 +51,9 @@ def test_resolved_environment_has_no_second_usd_provider():
5951
assert "usd-exchange" in locked
6052

6153

62-
def test_standalone_importers_are_opt_in():
54+
def test_standalone_importers_are_opt_in(source_checkout_root: Path):
6355
"""Standalone URDF/MJCF importers must not constrain the base environment."""
64-
with (_repo_root() / "pyproject.toml").open("rb") as f:
56+
with (source_checkout_root / "pyproject.toml").open("rb") as f:
6557
pyproject = tomllib.load(f)
6658

6759
project = pyproject["project"]

source/isaaclab/test/cli/test_teleop_entrypoints.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_teleop_workflow_help_exposes_task_preset_selectors(command, script_part
4747

4848

4949
@pytest.mark.parametrize(("command", "script_parts"), TELEOP_WORKFLOWS.items())
50-
def test_teleop_dispatches_to_an_existing_script(command, script_parts):
50+
def test_teleop_dispatches_to_an_existing_script(source_checkout_root: Path, command, script_parts):
5151
"""``isaaclab teleop`` forwards the remaining arguments to a script that exists."""
5252
args = [command, "--task", "IsaacContrib-PickPlace-Locomanipulation-G1-Abs", "--xr"]
5353

@@ -57,7 +57,7 @@ def test_teleop_dispatches_to_an_existing_script(command, script_parts):
5757
):
5858
cli.cli()
5959

60-
script = cli.ISAACLAB_ROOT.joinpath(*script_parts)
60+
script = source_checkout_root.joinpath(*script_parts)
6161
run_python.assert_called_once_with(script, args[1:], check=True)
6262
assert script.is_file()
6363

@@ -73,21 +73,21 @@ def _requirement_names(requirements: list[str]) -> set[str]:
7373

7474

7575
@pytest.mark.parametrize(("command", "script_parts"), TELEOP_WORKFLOWS.items())
76-
def test_teleop_workflow_isaaclab_imports_are_covered_by_the_extras(command, script_parts):
76+
def test_teleop_workflow_isaaclab_imports_are_covered_by_the_extras(source_checkout_root: Path, command, script_parts):
7777
"""Every ``isaaclab_*`` package a teleop script imports must ship with the extras.
7878
7979
``record_demos.py`` imports ``isaaclab_mimic`` at module level, so an environment built
8080
from ``--extra teleop`` alone used to die with ``ModuleNotFoundError`` only after Isaac Sim
8181
had finished booting. This catches that class of gap without needing an install.
8282
"""
83-
with (cli.ISAACLAB_ROOT / "pyproject.toml").open("rb") as f:
83+
with (source_checkout_root / "pyproject.toml").open("rb") as f:
8484
pyproject = tomllib.load(f)
8585
project = pyproject["project"]
8686

8787
available = _requirement_names(project["dependencies"])
8888
available |= _requirement_names(project["optional-dependencies"]["teleop"])
8989

90-
source = Path(cli.ISAACLAB_ROOT).joinpath(*script_parts).read_text(encoding="utf-8")
90+
source = source_checkout_root.joinpath(*script_parts).read_text(encoding="utf-8")
9191
imported = set(re.findall(r"^\s*(?:import|from)\s+(isaaclab_\w+)", source, re.MULTILINE))
9292

9393
missing = imported - available

source/isaaclab/test/cli/test_uv_run_pyproject.py

Lines changed: 26 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,26 @@
1616
pytestmark = pytest.mark.unit
1717

1818

19-
def _repo_root() -> Path:
20-
"""Find the Isaac Lab repository root from this test file."""
21-
for parent in Path(__file__).resolve().parents:
22-
if (parent / "pyproject.toml").is_file() and (parent / "source").is_dir():
23-
return parent
24-
raise RuntimeError("Could not find Isaac Lab repository root.")
25-
26-
27-
def _root_pyproject() -> dict:
19+
def _root_pyproject(source_checkout_root: Path) -> dict:
2820
"""Load the root development ``pyproject.toml``."""
29-
with (_repo_root() / "pyproject.toml").open("rb") as f:
21+
with (source_checkout_root / "pyproject.toml").open("rb") as f:
3022
return tomllib.load(f)
3123

3224

33-
def test_uv_run_extra_names_match_documented_workflow():
25+
def test_uv_run_extra_names_match_documented_workflow(source_checkout_root: Path):
3426
"""Docs must only reference ``uv run --extra`` names that pyproject defines."""
35-
repo_root = _repo_root()
27+
repo_root = source_checkout_root
3628
docs = (repo_root / "docs/source/setup/installation/index.rst").read_text(encoding="utf-8")
3729
documented_extras = set(re.findall(r"--extra\s+([A-Za-z0-9_-]+)", docs))
38-
optional_dependencies = _root_pyproject()["project"]["optional-dependencies"]
30+
optional_dependencies = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]
3931

4032
assert documented_extras
4133
assert documented_extras <= set(optional_dependencies)
4234

4335

44-
def test_uv_run_exposes_centralized_feature_extras():
36+
def test_uv_run_exposes_centralized_feature_extras(source_checkout_root: Path):
4537
"""The root project centralizes optional third-party deps into named extras."""
46-
optional_dependencies = _root_pyproject()["project"]["optional-dependencies"]
38+
optional_dependencies = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]
4739

4840
# Feature extras a user can activate with ``uv run --extra``.
4941
expected_extras = {
@@ -84,9 +76,9 @@ def test_uv_run_exposes_centralized_feature_extras():
8476
assert any(dep.startswith("ovstage") for dep in optional_dependencies["ovrtx"])
8577

8678

87-
def test_all_extra_aggregates_curated_ov_rl_and_visualizer_extras():
79+
def test_all_extra_aggregates_curated_ov_rl_and_visualizer_extras(source_checkout_root: Path):
8880
"""``all`` aggregates only the curated OV, RL, and visualizer extras."""
89-
optional = _root_pyproject()["project"]["optional-dependencies"]
81+
optional = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]
9082

9183
assert len(optional["all"]) == 1
9284
aggregated = set(re.fullmatch(r"isaaclab-dev\[(.+)\]", optional["all"][0]).group(1).split(","))
@@ -108,9 +100,9 @@ def test_all_extra_aggregates_curated_ov_rl_and_visualizer_extras():
108100
}
109101

110102

111-
def test_tetrahedralization_is_explicit_extra_only():
103+
def test_tetrahedralization_is_explicit_extra_only(source_checkout_root: Path):
112104
"""TetWild and its visualization stack are installed only when requested."""
113-
project = _root_pyproject()["project"]
105+
project = _root_pyproject(source_checkout_root)["project"]
114106
optional = project["optional-dependencies"]
115107

116108
assert not any(dep.startswith("pytetwild") for dep in project["dependencies"])
@@ -122,14 +114,14 @@ def test_tetrahedralization_is_explicit_extra_only():
122114
assert not any("tetrahedralization" in dep or dep.startswith("pytetwild") for dep in deps)
123115

124116

125-
def test_version_single_source_matches_literal_pins():
117+
def test_version_single_source_matches_literal_pins(source_checkout_root: Path):
126118
"""``[tool.isaaclab.versions]`` is the single source for externally-pinned versions.
127119
128120
TOML cannot interpolate, so the literal pins in ``[project.dependencies]``,
129121
``[project.optional-dependencies]``, and ``[tool.uv].override-dependencies`` must
130122
mirror the table exactly. This test fails if any of them drift apart.
131123
"""
132-
pyproject = _root_pyproject()
124+
pyproject = _root_pyproject(source_checkout_root)
133125
versions = pyproject["tool"]["isaaclab"]["versions"]
134126
dependencies = pyproject["project"]["dependencies"]
135127
optional = pyproject["project"]["optional-dependencies"]
@@ -156,7 +148,7 @@ def spec(package: str) -> str:
156148
# ovrtx`` ignores this ceiling). Each such install must therefore be pinned:
157149
# either by carrying the literal range, or by referencing the ``resolve-ov-pins``
158150
# action output, which reads the pin from this same table. Never a bare ``ovrtx``.
159-
build_workflow = (_repo_root() / ".github/workflows/build.yaml").read_text(encoding="utf-8")
151+
build_workflow = (source_checkout_root / ".github/workflows/build.yaml").read_text(encoding="utf-8")
160152
assert "ovphysx==0.4.13" not in build_workflow
161153
ovrtx_install_lines = [
162154
line.strip() for line in build_workflow.splitlines() if "extra-pip-packages:" in line and "ovrtx" in line
@@ -179,9 +171,9 @@ def spec(package: str) -> str:
179171
assert warp_spec in dependencies
180172

181173

182-
def test_public_ov_packages_use_public_pypi_index():
174+
def test_public_ov_packages_use_public_pypi_index(source_checkout_root: Path):
183175
"""Public OV packages must not resolve from the NVIDIA package index."""
184-
pyproject = _root_pyproject()
176+
pyproject = _root_pyproject(source_checkout_root)
185177
indexes = {index.get("name"): index for index in pyproject["tool"]["uv"]["index"]}
186178
sources = pyproject["tool"]["uv"]["sources"]
187179

@@ -194,7 +186,7 @@ def test_public_ov_packages_use_public_pypi_index():
194186
assert sources[package] == {"index": "pypi-public"}
195187

196188

197-
def test_uv_run_declares_no_extra_conflicts():
189+
def test_uv_run_declares_no_extra_conflicts(source_checkout_root: Path):
198190
"""No extra is forked: every combination resolves into a single environment.
199191
200192
``[tool.uv].conflicts`` used to fork ``isaacsim`` / ``teleop`` away from the OV runtimes,
@@ -203,21 +195,21 @@ def test_uv_run_declares_no_extra_conflicts():
203195
importers install beside Isaac Sim without displacing it: the two distributions share no
204196
files, and Kit serves ``isaacsim.asset`` from its extension roots either way.
205197
"""
206-
tool_uv = _root_pyproject()["tool"]["uv"]
198+
tool_uv = _root_pyproject(source_checkout_root)["tool"]["uv"]
207199

208200
assert "conflicts" not in tool_uv
209201
for override in ("packaging>=20,<27", "websockets>=14.0,<17.0.0", "coverage>=7.6.1"):
210202
assert override in tool_uv["override-dependencies"]
211203

212204

213-
def test_uv_run_isaacsim_is_an_opt_in_extra():
205+
def test_uv_run_isaacsim_is_an_opt_in_extra(source_checkout_root: Path):
214206
"""Isaac Sim is never a base dependency, but it is a real workspace extra.
215207
216208
PhysX/Isaac Sim must stay out of ``[project.dependencies]`` so the bare ``uv run``
217209
keeps working without Kit, while still being an ``optional-dependencies`` entry so
218210
``uv run --extra isaacsim`` resolves.
219211
"""
220-
pyproject = _root_pyproject()
212+
pyproject = _root_pyproject(source_checkout_root)
221213
project = pyproject["project"]
222214
base_dependency_names = {re.split(r"[\s<>=!~\[;]", dep, maxsplit=1)[0] for dep in project["dependencies"]}
223215

@@ -230,14 +222,14 @@ def test_uv_run_isaacsim_is_an_opt_in_extra():
230222
assert "wheel-extras" not in pyproject.get("tool", {}).get("isaaclab", {})
231223

232224

233-
def test_uv_run_teleop_extra_excludes_isaacsim():
225+
def test_uv_run_teleop_extra_excludes_isaacsim(source_checkout_root: Path):
234226
"""``teleop`` carries the teleop stack only; Isaac Sim stays its own extra.
235227
236228
XR teleop needs the Kit XR runtime, but environments that already provide Kit (the
237229
container images) must not install a second copy of it. Users who need the wheel run
238230
``--extra teleop,isaacsim``.
239231
"""
240-
optional_dependencies = _root_pyproject()["project"]["optional-dependencies"]
232+
optional_dependencies = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]
241233
teleop = optional_dependencies["teleop"]
242234

243235
assert not any(dep.startswith("isaacsim") for dep in teleop)
@@ -247,18 +239,18 @@ def test_uv_run_teleop_extra_excludes_isaacsim():
247239
assert not any(dep.startswith("robomimic") for dep in teleop)
248240

249241

250-
def test_uv_run_base_dependencies_cover_newton_rsl_rl_training():
242+
def test_uv_run_base_dependencies_cover_newton_rsl_rl_training(source_checkout_root: Path):
251243
"""The documented bare ``uv run isaaclab train`` command needs Newton and RSL-RL in core."""
252-
dependencies = _root_pyproject()["project"]["dependencies"]
244+
dependencies = _root_pyproject(source_checkout_root)["project"]["dependencies"]
253245

254246
# Newton is the default physics engine and RSL-RL the default training library,
255247
# so both ship as core third-party requirements (not opt-in extras).
256248
assert any(dep.startswith("newton[sim]") for dep in dependencies)
257249
assert any(dep.startswith("rsl-rl-lib") for dep in dependencies)
258250

259251

260-
def test_uv_run_uses_managed_python():
252+
def test_uv_run_uses_managed_python(source_checkout_root: Path):
261253
"""Avoid building the project venv from conda Python and its older C++ runtime."""
262-
tool_uv = _root_pyproject()["tool"]["uv"]
254+
tool_uv = _root_pyproject(source_checkout_root)["tool"]["uv"]
263255

264256
assert tool_uv["python-preference"] == "only-managed"

0 commit comments

Comments
 (0)