Skip to content

Commit b82b93b

Browse files
committed
Fix IsaacTeleop retargeting support detection
Check for RetargetingExecutionConfig and DeadlinePacingConfig before enabling the pipelined retargeting default. Older IsaacTeleop packages can expose teleop_session_manager without these newer configuration classes, so import success alone is not enough. Update the CloudXR lifecycle tests to cover both supported and legacy IsaacTeleop APIs while keeping their heavy dependency stubs isolated.
1 parent 483ca6b commit b82b93b

2 files changed

Lines changed: 84 additions & 6 deletions

File tree

source/isaaclab_teleop/isaaclab_teleop/isaac_teleop_cfg.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,21 @@
3030
from isaacteleop.teleop_session_manager import PluginConfig
3131

3232

33+
def _retargeting_execution_is_supported(teleop_session_manager: Any | None) -> bool:
34+
"""Check whether IsaacTeleop exposes retargeting execution configuration."""
35+
return (
36+
teleop_session_manager is not None
37+
and hasattr(teleop_session_manager, "RetargetingExecutionConfig")
38+
and hasattr(teleop_session_manager, "DeadlinePacingConfig")
39+
)
40+
41+
3342
try:
3443
import isaacteleop.teleop_session_manager as _tsm
35-
36-
_RETARGETING_EXECUTION_SUPPORTED = True
3744
except ImportError:
3845
_tsm = None
39-
_RETARGETING_EXECUTION_SUPPORTED = False
46+
47+
_RETARGETING_EXECUTION_SUPPORTED = _retargeting_execution_is_supported(_tsm)
4048

4149

4250
def _default_retargeting_execution_config() -> Any | None:

source/isaaclab_teleop/test/test_cloudxr_lifecycle.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,10 @@ def _install_stubs():
7171
"""Insert MagicMock modules for all heavy dependencies."""
7272
for name in _MODULES_TO_STUB:
7373
if name not in sys.modules:
74-
_stubs_installed[name] = MagicMock()
75-
sys.modules[name] = _stubs_installed[name]
74+
sys.modules[name] = _stubs_installed.setdefault(name, MagicMock())
75+
if "." in name:
76+
parent_name, child_name = name.rsplit(".", 1)
77+
setattr(sys.modules[parent_name], child_name, sys.modules[name])
7678

7779
@dataclass
7880
class DeadlinePacingConfig:
@@ -88,6 +90,21 @@ class RetargetingExecutionConfig:
8890
tsm.RetargetingExecutionConfig = RetargetingExecutionConfig # type: ignore[attr-defined]
8991

9092

93+
def _restore_stubs():
94+
"""Remove stubs installed for this test module from ``sys.modules``."""
95+
for name in reversed(_MODULES_TO_STUB):
96+
stub = _stubs_installed.get(name)
97+
if stub is None:
98+
continue
99+
if "." in name:
100+
parent_name, child_name = name.rsplit(".", 1)
101+
parent = sys.modules.get(parent_name)
102+
if parent is not None and getattr(parent, child_name, None) is stub:
103+
delattr(parent, child_name)
104+
if sys.modules.get(name) is stub:
105+
del sys.modules[name]
106+
107+
91108
_install_stubs()
92109

93110
from isaaclab_teleop.isaac_teleop_cfg import ( # noqa: E402
@@ -97,11 +114,21 @@ class RetargetingExecutionConfig:
97114
)
98115
from isaaclab_teleop.session_lifecycle import TeleopSessionLifecycle # noqa: E402
99116

117+
_restore_stubs()
118+
100119
# ---------------------------------------------------------------------------
101120
# Helpers
102121
# ---------------------------------------------------------------------------
103122

104123

124+
@pytest.fixture(autouse=True)
125+
def _stub_heavy_dependencies():
126+
"""Keep CloudXR tests isolated from modules collected later in the suite."""
127+
_install_stubs()
128+
yield
129+
_restore_stubs()
130+
131+
105132
def _make_cfg() -> IsaacTeleopCfg:
106133
"""Build a minimal IsaacTeleopCfg with a dummy pipeline_builder."""
107134
return IsaacTeleopCfg(
@@ -123,6 +150,24 @@ def _make_lifecycle(
123150
)
124151

125152

153+
def _make_supported_tsm_module() -> ModuleType:
154+
"""Build a fake ``teleop_session_manager`` with the new execution config API."""
155+
156+
@dataclass
157+
class DeadlinePacingConfig:
158+
safety_margin_s: float = 0.025
159+
160+
@dataclass
161+
class RetargetingExecutionConfig:
162+
mode: str = "sync"
163+
pacing: DeadlinePacingConfig | None = None
164+
165+
tsm = ModuleType("isaacteleop.teleop_session_manager")
166+
tsm.DeadlinePacingConfig = DeadlinePacingConfig # type: ignore[attr-defined]
167+
tsm.RetargetingExecutionConfig = RetargetingExecutionConfig # type: ignore[attr-defined]
168+
return tsm
169+
170+
126171
# ============================================================================
127172
# Shipped .env profile paths
128173
# ============================================================================
@@ -163,7 +208,13 @@ class TestRetargetingExecutionConfig:
163208

164209
def test_cfg_defaults_to_deadline_paced_pipelined_retargeting(self):
165210
"""Isaac Lab defaults to deadline-paced pipelined retargeting."""
166-
cfg = _make_cfg()
211+
import isaaclab_teleop.isaac_teleop_cfg as cfg_module
212+
213+
with (
214+
patch.object(cfg_module, "_RETARGETING_EXECUTION_SUPPORTED", True),
215+
patch.object(cfg_module, "_tsm", _make_supported_tsm_module()),
216+
):
217+
cfg = _make_cfg()
167218

168219
assert cfg.retargeting_execution.mode == "pipelined"
169220
assert cfg.retargeting_execution.pacing.safety_margin_s == 0.025
@@ -177,8 +228,23 @@ def test_cfg_defaults_to_none_with_legacy_isaacteleop(self):
177228

178229
assert cfg.retargeting_execution is None
179230

231+
def test_retargeting_execution_support_requires_new_config_classes(self):
232+
"""IsaacTeleop must expose both execution config classes to enable the default."""
233+
import isaaclab_teleop.isaac_teleop_cfg as cfg_module
234+
235+
legacy_tsm = ModuleType("isaacteleop.teleop_session_manager")
236+
assert not cfg_module._retargeting_execution_is_supported(legacy_tsm)
237+
238+
legacy_tsm.RetargetingExecutionConfig = object
239+
assert not cfg_module._retargeting_execution_is_supported(legacy_tsm)
240+
241+
legacy_tsm.DeadlinePacingConfig = object
242+
assert cfg_module._retargeting_execution_is_supported(legacy_tsm)
243+
180244
def test_session_config_receives_cfg_retargeting_execution(self):
181245
"""The configured IsaacTeleop execution mode is passed into TeleopSession."""
246+
import isaaclab_teleop.session_lifecycle as lifecycle_module
247+
182248
cfg = _make_cfg()
183249
sentinel_execution = object()
184250
cfg.retargeting_execution = sentinel_execution
@@ -192,6 +258,7 @@ def test_session_config_receives_cfg_retargeting_execution(self):
192258
fake_tsm_module = sys.modules["isaacteleop.teleop_session_manager"]
193259

194260
with (
261+
patch.object(lifecycle_module, "_RETARGETING_EXECUTION_SUPPORTED", True),
195262
patch.object(fake_tsm_module, "TeleopSessionConfig", session_config_cls),
196263
patch.object(fake_tsm_module, "TeleopSession", session_cls),
197264
patch.object(lifecycle, "_ensure_xr_ar_profile_enabled"),
@@ -235,6 +302,8 @@ def test_session_config_omits_retargeting_execution_for_legacy_isaacteleop(self,
235302

236303
def test_session_config_propagates_typeerror_from_supported_isaacteleop(self):
237304
"""A TypeError from a supported IsaacTeleop must surface, not be swallowed."""
305+
import isaaclab_teleop.session_lifecycle as lifecycle_module
306+
238307
cfg = _make_cfg()
239308
cfg.retargeting_execution = object()
240309

@@ -248,6 +317,7 @@ def raising_session_config(**kwargs):
248317
fake_tsm_module = sys.modules["isaacteleop.teleop_session_manager"]
249318

250319
with (
320+
patch.object(lifecycle_module, "_RETARGETING_EXECUTION_SUPPORTED", True),
251321
patch.object(fake_tsm_module, "TeleopSessionConfig", raising_session_config),
252322
patch.object(fake_tsm_module, "TeleopSession", MagicMock()),
253323
patch.object(lifecycle, "_ensure_xr_ar_profile_enabled"),

0 commit comments

Comments
 (0)