Skip to content
Draft
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
9 changes: 8 additions & 1 deletion .github/actions/run-package-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ inputs:
description: 'Additional pytest options'
default: ''
required: false
test-path:
description: >-
Path handed to pytest. Defaults to "tools", which loads tools/conftest.py and runs each
test file in its own subprocess. Point it at a test directory instead to run those files
together in a single pytest process, bypassing the per-file orchestrator.
default: 'tools'
required: false
extra-pip-packages:
description: 'Space-separated pip packages to install inside the Docker container before pytest starts'
default: ''
Expand Down Expand Up @@ -291,7 +298,7 @@ runs:
- name: Run Tests
uses: ./.github/actions/run-tests
with:
test-path: "tools"
test-path: ${{ inputs.test-path }}
result-file: "${{ inputs.result-file != '' && inputs.result-file || format('{0}-report.xml', github.job) }}"
container-name: "${{ inputs.container-name }}-${{ github.run_id }}-${{ github.run_attempt }}"
image-tag: ${{ inputs.image-tag }}
Expand Down
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ markers = [
"benchmark: test covers the Isaac Lab benchmark framework and infrastructure",
"rendering: test exercises the rendering / camera / visualizer pipeline",
"smoke: tests for core installation, task, and RL functionality",
"kit: test file needs a booted headless Kit app; it calls isaaclab.test.launch.launch_kit() at module scope rather than constructing AppLauncher",
"kit_cameras: like `kit`, but the app is booted with cameras enabled via launch_kit(cameras=True)",
"kitless: test file runs without Kit; no AppLauncher and no module-scope import of omni/carb/isaacsim",
"kit_solo: keep this file in its own process; it is never grouped with other files",
"newton_ci: mark test to run in the Newton CI lane",
]

# Add pypi.nvidia.com so that `uv pip install isaaclab[isaacsim]` works without --extra-index-url.
Expand Down
15 changes: 15 additions & 0 deletions source/isaaclab/changelog.d/mataylor-kit-test-markers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Added
^^^^^

* Added :func:`~isaaclab.test.launch.launch_kit` so test modules can share one Kit app per
pytest process instead of each launching their own. It is idempotent: the first module to
call it boots Kit and later modules receive the running app.
* Added the ``kit``, ``kit_cameras``, ``kitless``, and ``kit_solo`` pytest markers so a test
file can declare its Kit launch configuration, plus a test that checks each file's markers
against what it actually does at module scope.

Fixed
^^^^^

* Fixed ``test_operational_space.py`` assigning ``pytestmark`` twice, which silently dropped
its ``arm_ci`` marker and kept the file out of the ARM CI lane.
94 changes: 94 additions & 0 deletions source/isaaclab/isaaclab/test/launch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause

"""Shared Kit launch helper for Isaac Lab tests.

Test modules that need Isaac Sim call :func:`launch_kit` at module scope in place of
constructing :class:`~isaaclab.app.AppLauncher` directly::

from isaaclab.test.launch import launch_kit

launch_kit() # or launch_kit(cameras=True)

The call must stay at module scope: a test module's own imports (``pxr``, ``omni``,
``isaaclab_physx``, ...) run during pytest collection, before any fixture executes, so Kit
must already be running by then.

:func:`launch_kit` is idempotent within a process. The first test module to call it boots
Kit; every later module gets the running app back. A pytest process covering several test
files therefore pays Kit startup once rather than once per file.

Declare the matching marker on the module so the test runner can group files that share a
launch configuration into one process::

pytestmark = pytest.mark.kit # launch_kit()
pytestmark = pytest.mark.kit_cameras # launch_kit(cameras=True)

The two groups cannot be merged. Cameras cannot be enabled after startup, so a plain ``kit``
file cannot run in a process a ``kit_cameras`` file will later join; and a camera-enabled app
is not a drop-in replacement for a plain one either, because some tests assert that offscreen
rendering is off. :func:`launch_kit` therefore raises on any mismatch rather than handing back
an app whose configuration is not the one the caller asked for.
"""

from __future__ import annotations

from typing import Any

_app: Any = None
"""The Kit application booted by :func:`launch_kit`, or None before the first call."""

_cameras: bool = False
"""Whether :attr:`_app` was booted with camera and render extensions enabled."""


def launch_kit(*, cameras: bool = False) -> Any:
"""Boot the shared Kit app for this process, or return the one already running.

Args:
cameras: Whether the app must be booted with camera and render extensions enabled.
Passed through to :paramref:`~isaaclab.app.AppLauncher.enable_cameras`.

Returns:
The running ``SimulationApp``.

Raises:
RuntimeError: If the running app was booted with a different ``cameras`` setting, or if
Kit was started by something other than this function. Both mean the test files
sharing this process do not share a launch configuration and must be split across
processes.
"""
global _app, _cameras

if _app is not None:
if cameras != _cameras:
wanted = "with" if cameras else "without"
running = "with" if _cameras else "without"
raise RuntimeError(
f"launch_kit(cameras={cameras}) wants an app {wanted} cameras, but Kit is already"
f" running in this process {running} them, and that cannot be changed after"
" startup. Files marked `kit` and `kit_cameras` need separate processes. A"
" camera-enabled app is not a drop-in replacement for a plain one:"
" test_simulation_context.py::test_headless_mode asserts that offscreen"
" rendering is off."
)
return _app

from isaaclab.utils import has_kit

if has_kit():
raise RuntimeError(
"Kit is already running but was not started by launch_kit(), so its launch"
" configuration is unknown. Another test file in this process still constructs"
" AppLauncher directly; run that file in its own process."
)

from isaaclab.app import AppLauncher

from .utils import resolve_test_sim_device

_app = AppLauncher(headless=True, enable_cameras=cameras, device=resolve_test_sim_device()).app
_cameras = cameras
return _app
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import torch
from flaky import flaky

pytestmark = pytest.mark.arm_ci

import isaaclab.envs.mdp as mdp
import isaaclab.sim as sim_utils
from isaaclab import cloner
Expand Down Expand Up @@ -51,7 +49,7 @@

from isaaclab_assets import FRANKA_PANDA_CFG, G1_29DOF_CFG # isort:skip

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.arm_ci, pytest.mark.integration]


@pytest.fixture
Expand Down
11 changes: 4 additions & 7 deletions source/isaaclab/test/sim/test_articulation_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import os

Expand All @@ -21,6 +16,8 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.kit


def _make_xform(stage, path="/World/Art"):
UsdGeom.Xform.Define(stage, path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,16 @@
``test_build_simulation_context_nonheadless.py``.
"""

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

from isaaclab.sim.simulation_cfg import SimulationCfg
from isaaclab.sim.simulation_context import build_simulation_context

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


@pytest.mark.parametrize("gravity_enabled", [True, False])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,16 @@
``test_build_simulation_context_headless.py``.
"""

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

from isaaclab.sim.simulation_cfg import SimulationCfg
from isaaclab.sim.simulation_context import build_simulation_context

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


@pytest.mark.parametrize("gravity_enabled", [True, False])
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_cloner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@

"""Tests for USD cloner utilities (no PhysX dependency)."""

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

from types import SimpleNamespace
from unittest.mock import MagicMock
Expand All @@ -37,7 +32,7 @@
)
from isaaclab.sim import build_simulation_context

pytestmark = [pytest.mark.integration, pytest.mark.isaacsim_ci]
pytestmark = [pytest.mark.kit, pytest.mark.integration, pytest.mark.isaacsim_ci]


@pytest.fixture(params=["cpu", "cuda"])
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_collision_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

Expand All @@ -19,7 +14,7 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


def _make_xform(stage, path="/World/Body"):
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_joint_drive_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import math

Expand All @@ -21,7 +16,7 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


def _make_revolute_joint(stage, path="/World/Articulation/joint_0"):
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_mass_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

Expand All @@ -19,7 +14,7 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


def _make_xform(stage, path="/World/Body"):
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_material_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

Expand All @@ -19,7 +14,7 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]

# -------------------------------------------------------------------------------------
# RigidBodyMaterialFragment marker + metadata
Expand Down
11 changes: 3 additions & 8 deletions source/isaaclab/test/sim/test_mesh_collision_fragments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,9 @@
#
# SPDX-License-Identifier: BSD-3-Clause

"""Launch Isaac Sim Simulator first."""
from isaaclab.test.launch import launch_kit

from isaaclab.app import AppLauncher

# launch omniverse app
simulation_app = AppLauncher(headless=True).app

"""Rest everything follows."""
launch_kit()

import pytest

Expand All @@ -19,7 +14,7 @@
import isaaclab.sim as sim_utils
from isaaclab.sim import SimulationCfg, SimulationContext

pytestmark = pytest.mark.integration
pytestmark = [pytest.mark.kit, pytest.mark.integration]


def _make_xform(stage, path="/World/Mesh"):
Expand Down
Loading
Loading