Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test-only: added a source checkout root fixture for tests that inspect repository artifacts.
14 changes: 6 additions & 8 deletions source/isaaclab/test/app/test_experience_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,17 @@

pytestmark = pytest.mark.unit

APPS_DIR = Path(__file__).resolve().parents[4] / "apps"

# ``.kit`` files repeat section headers, so they cannot be parsed as TOML.
_SECTION_RE = re.compile(r"^\s*\[+(?P<name>[^\[\]]+)\]+\s*$")
_DEPENDENCY_RE = re.compile(r'^\s*"(?P<name>[^"]+)"\s*=')


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

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


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


@pytest.mark.parametrize(
Expand All @@ -52,6 +50,6 @@ def test_base_experiences_enable_native_storage(experience: str):
("isaaclab.python.xr.openxr.headless.kit", "isaaclab.python.xr.openxr"),
],
)
def test_derived_experiences_inherit_native_storage(experience: str, base: str):
def test_derived_experiences_inherit_native_storage(source_checkout_root: Path, experience: str, base: str):
"""Test the derived experiences inherit native storage from a base experience."""
assert base in _kit_dependencies(experience)
assert base in _kit_dependencies(source_checkout_root / "apps", experience)
5 changes: 3 additions & 2 deletions source/isaaclab/test/cli/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,11 +433,12 @@ class TestPinkIkStack:
stack from there instead of mirroring the versions.
"""

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

stack = install._pink_ik_stack()
with mock.patch.object(install, "ISAACLAB_ROOT", source_checkout_root):
stack = install._pink_ik_stack()
assert [install._requirement_name(r) for r in stack] == list(install._PINK_IK_PACKAGES)
assert any(r.startswith("pin-pink==") for r in stack), "pin-pink must stay exactly pinned"
assert any(r.startswith("daqp==") for r in stack), "daqp must stay exactly pinned"
Expand Down
20 changes: 6 additions & 14 deletions source/isaaclab/test/cli/test_source_package_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,14 @@
pytestmark = pytest.mark.unit


def _repo_root() -> Path:
"""Find the Isaac Lab repository root from this test file."""
for parent in Path(__file__).resolve().parents:
if (parent / "pyproject.toml").is_file() and (parent / "source").is_dir():
return parent
raise RuntimeError("Could not find Isaac Lab repository root.")


def test_isaaclab_uses_one_standalone_usd_provider():
def test_isaaclab_uses_one_standalone_usd_provider(source_checkout_root: Path):
"""Isaac Lab must install only the USD provider shared with its importer dependencies.

``usd-core`` and ``usd-exchange`` each install a complete ``pxr`` into the same directory, so
a second provider silently overwrites the first and removing either one leaves ``pxr`` broken.
Nothing detects that, because the two are separate distributions.
"""
with (_repo_root() / "pyproject.toml").open("rb") as f:
with (source_checkout_root / "pyproject.toml").open("rb") as f:
pyproject = tomllib.load(f)

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


def test_resolved_environment_has_no_second_usd_provider():
def test_resolved_environment_has_no_second_usd_provider(source_checkout_root: Path):
"""No dependency may pull ``usd-core`` back in behind an extra.

``newton[importers]``, ``mujoco[usd]`` and ``warp-lang[examples]`` all require it, so selecting
any of them would reinstate the overlap that the direct dependencies avoid. Checking the lock
catches that, where checking ``pyproject.toml`` alone would not.
"""
with (_repo_root() / "uv.lock").open("rb") as f:
with (source_checkout_root / "uv.lock").open("rb") as f:
lock = tomllib.load(f)

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


def test_standalone_importers_are_opt_in():
def test_standalone_importers_are_opt_in(source_checkout_root: Path):
"""Standalone URDF/MJCF importers must not constrain the base environment."""
with (_repo_root() / "pyproject.toml").open("rb") as f:
with (source_checkout_root / "pyproject.toml").open("rb") as f:
pyproject = tomllib.load(f)

project = pyproject["project"]
Expand Down
10 changes: 5 additions & 5 deletions source/isaaclab/test/cli/test_teleop_entrypoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def test_teleop_workflow_help_exposes_task_preset_selectors(command, script_part


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

Expand All @@ -57,7 +57,7 @@ def test_teleop_dispatches_to_an_existing_script(command, script_parts):
):
cli.cli()

script = cli.ISAACLAB_ROOT.joinpath(*script_parts)
script = source_checkout_root.joinpath(*script_parts)
run_python.assert_called_once_with(script, args[1:], check=True)
assert script.is_file()

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


@pytest.mark.parametrize(("command", "script_parts"), TELEOP_WORKFLOWS.items())
def test_teleop_workflow_isaaclab_imports_are_covered_by_the_extras(command, script_parts):
def test_teleop_workflow_isaaclab_imports_are_covered_by_the_extras(source_checkout_root: Path, command, script_parts):
"""Every ``isaaclab_*`` package a teleop script imports must ship with the extras.

``record_demos.py`` imports ``isaaclab_mimic`` at module level, so an environment built
from ``--extra teleop`` alone used to die with ``ModuleNotFoundError`` only after Isaac Sim
had finished booting. This catches that class of gap without needing an install.
"""
with (cli.ISAACLAB_ROOT / "pyproject.toml").open("rb") as f:
with (source_checkout_root / "pyproject.toml").open("rb") as f:
pyproject = tomllib.load(f)
project = pyproject["project"]

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

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

missing = imported - available
Expand Down
60 changes: 26 additions & 34 deletions source/isaaclab/test/cli/test_uv_run_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,26 @@
pytestmark = pytest.mark.unit


def _repo_root() -> Path:
"""Find the Isaac Lab repository root from this test file."""
for parent in Path(__file__).resolve().parents:
if (parent / "pyproject.toml").is_file() and (parent / "source").is_dir():
return parent
raise RuntimeError("Could not find Isaac Lab repository root.")


def _root_pyproject() -> dict:
def _root_pyproject(source_checkout_root: Path) -> dict:
"""Load the root development ``pyproject.toml``."""
with (_repo_root() / "pyproject.toml").open("rb") as f:
with (source_checkout_root / "pyproject.toml").open("rb") as f:
return tomllib.load(f)


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

assert documented_extras
assert documented_extras <= set(optional_dependencies)


def test_uv_run_exposes_centralized_feature_extras():
def test_uv_run_exposes_centralized_feature_extras(source_checkout_root: Path):
"""The root project centralizes optional third-party deps into named extras."""
optional_dependencies = _root_pyproject()["project"]["optional-dependencies"]
optional_dependencies = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]

# Feature extras a user can activate with ``uv run --extra``.
expected_extras = {
Expand Down Expand Up @@ -83,9 +75,9 @@ def test_uv_run_exposes_centralized_feature_extras():
assert any(dep.startswith("ovstage") for dep in optional_dependencies["ovrtx"])


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

assert len(optional["all"]) == 1
aggregated = set(re.fullmatch(r"isaaclab-dev\[(.+)\]", optional["all"][0]).group(1).split(","))
Expand All @@ -106,9 +98,9 @@ def test_all_extra_aggregates_curated_ov_rl_and_visualizer_extras():
}


def test_tetrahedralization_is_explicit_extra_only():
def test_tetrahedralization_is_explicit_extra_only(source_checkout_root: Path):
"""TetWild and its visualization stack are installed only when requested."""
project = _root_pyproject()["project"]
project = _root_pyproject(source_checkout_root)["project"]
optional = project["optional-dependencies"]

assert not any(dep.startswith("pytetwild") for dep in project["dependencies"])
Expand All @@ -120,14 +112,14 @@ def test_tetrahedralization_is_explicit_extra_only():
assert not any("tetrahedralization" in dep or dep.startswith("pytetwild") for dep in deps)


def test_version_single_source_matches_literal_pins():
def test_version_single_source_matches_literal_pins(source_checkout_root: Path):
"""``[tool.isaaclab.versions]`` is the single source for externally-pinned versions.

TOML cannot interpolate, so the literal pins in ``[project.dependencies]``,
``[project.optional-dependencies]``, and ``[tool.uv].override-dependencies`` must
mirror the table exactly. This test fails if any of them drift apart.
"""
pyproject = _root_pyproject()
pyproject = _root_pyproject(source_checkout_root)
versions = pyproject["tool"]["isaaclab"]["versions"]
dependencies = pyproject["project"]["dependencies"]
optional = pyproject["project"]["optional-dependencies"]
Expand Down Expand Up @@ -155,7 +147,7 @@ def spec(package: str) -> str:
# ovrtx`` ignores this ceiling). Each such install must therefore be pinned:
# either by carrying the literal range, or by referencing the ``resolve-ov-pins``
# action output, which reads the pin from this same table. Never a bare ``ovrtx``.
build_workflow = (_repo_root() / ".github/workflows/build.yaml").read_text(encoding="utf-8")
build_workflow = (source_checkout_root / ".github/workflows/build.yaml").read_text(encoding="utf-8")
assert "ovphysx==0.4.13" not in build_workflow
ovrtx_install_lines = [
line.strip() for line in build_workflow.splitlines() if "extra-pip-packages:" in line and "ovrtx" in line
Expand All @@ -178,9 +170,9 @@ def spec(package: str) -> str:
assert warp_spec in dependencies


def test_public_ov_packages_use_public_pypi_index():
def test_public_ov_packages_use_public_pypi_index(source_checkout_root: Path):
"""Public OV packages must not resolve from the NVIDIA package index."""
pyproject = _root_pyproject()
pyproject = _root_pyproject(source_checkout_root)
indexes = {index.get("name"): index for index in pyproject["tool"]["uv"]["index"]}
sources = pyproject["tool"]["uv"]["sources"]

Expand All @@ -193,7 +185,7 @@ def test_public_ov_packages_use_public_pypi_index():
assert sources[package] == {"index": "pypi-public"}


def test_uv_run_declares_no_extra_conflicts():
def test_uv_run_declares_no_extra_conflicts(source_checkout_root: Path):
"""No extra is forked: every combination resolves into a single environment.

``[tool.uv].conflicts`` used to fork ``isaacsim`` / ``teleop`` away from the OV runtimes,
Expand All @@ -202,21 +194,21 @@ def test_uv_run_declares_no_extra_conflicts():
importers install beside Isaac Sim without displacing it: the two distributions share no
files, and Kit serves ``isaacsim.asset`` from its extension roots either way.
"""
tool_uv = _root_pyproject()["tool"]["uv"]
tool_uv = _root_pyproject(source_checkout_root)["tool"]["uv"]

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


def test_uv_run_isaacsim_is_an_opt_in_extra():
def test_uv_run_isaacsim_is_an_opt_in_extra(source_checkout_root: Path):
"""Isaac Sim is never a base dependency, but it is a real workspace extra.

PhysX/Isaac Sim must stay out of ``[project.dependencies]`` so the bare ``uv run``
keeps working without Kit, while still being an ``optional-dependencies`` entry so
``uv run --extra isaacsim`` resolves.
"""
pyproject = _root_pyproject()
pyproject = _root_pyproject(source_checkout_root)
project = pyproject["project"]
base_dependency_names = {re.split(r"[\s<>=!~\[;]", dep, maxsplit=1)[0] for dep in project["dependencies"]}

Expand All @@ -229,13 +221,13 @@ def test_uv_run_isaacsim_is_an_opt_in_extra():
assert "wheel-extras" not in pyproject.get("tool", {}).get("isaaclab", {})


def test_uv_run_teleop_extra_bundles_isaacsim():
def test_uv_run_teleop_extra_bundles_isaacsim(source_checkout_root: Path):
"""``--extra teleop`` is the single flag for the XR teleoperation workflow.

XR teleop needs the Kit XR runtime, so the extra carries Isaac Sim through the
``isaacsim`` extra rather than repeating its pin.
"""
optional_dependencies = _root_pyproject()["project"]["optional-dependencies"]
optional_dependencies = _root_pyproject(source_checkout_root)["project"]["optional-dependencies"]
teleop = optional_dependencies["teleop"]

# Isaac Sim is listed explicitly; test_version_single_source keeps the pin from drifting.
Expand All @@ -245,18 +237,18 @@ def test_uv_run_teleop_extra_bundles_isaacsim():
assert not any(dep.startswith("robomimic") for dep in teleop)


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

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


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

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