fix: 安全关闭 MuJoCo 显示窗口
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
from xr_rm_mujoco import dual_arm_simulator as simulator_module
|
||||||
from xr_rm_mujoco.dual_arm_simulator import (
|
from xr_rm_mujoco.dual_arm_simulator import (
|
||||||
ARM_JOINT_NAMES,
|
ARM_JOINT_NAMES,
|
||||||
DualArmKinematicModel,
|
DualArmKinematicModel,
|
||||||
@@ -70,6 +73,34 @@ def test_mujoco_config_contains_only_render_parameters() -> None:
|
|||||||
assert parameters == {"render_rate_hz": 60.0}
|
assert parameters == {"render_rate_hz": 60.0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_main_closes_viewer_cleanly_on_keyboard_interrupt(monkeypatch) -> None:
|
||||||
|
close_viewer = simulator_module.DualArmSimulator.close_viewer
|
||||||
|
viewer = Mock()
|
||||||
|
node = SimpleNamespace(_viewer=viewer, destroy_node=Mock())
|
||||||
|
node.close_viewer = lambda: close_viewer(node)
|
||||||
|
init = Mock()
|
||||||
|
spin = Mock(side_effect=KeyboardInterrupt)
|
||||||
|
sleep = Mock()
|
||||||
|
shutdown = Mock()
|
||||||
|
|
||||||
|
monkeypatch.setattr(simulator_module, "DualArmSimulator", lambda: node)
|
||||||
|
monkeypatch.setattr(simulator_module.rclpy, "init", init)
|
||||||
|
monkeypatch.setattr(simulator_module.rclpy, "spin", spin)
|
||||||
|
monkeypatch.setattr(simulator_module.rclpy, "ok", lambda: True)
|
||||||
|
monkeypatch.setattr(simulator_module.time, "sleep", sleep)
|
||||||
|
monkeypatch.setattr(simulator_module.rclpy, "shutdown", shutdown)
|
||||||
|
|
||||||
|
simulator_module.main(["--test"])
|
||||||
|
|
||||||
|
init.assert_called_once_with(args=["--test"])
|
||||||
|
spin.assert_called_once_with(node)
|
||||||
|
viewer.close.assert_called_once_with()
|
||||||
|
sleep.assert_called_once_with(0.1)
|
||||||
|
node.destroy_node.assert_called_once_with()
|
||||||
|
shutdown.assert_called_once_with()
|
||||||
|
assert node._viewer is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("names", "positions", "match"),
|
("names", "positions", "match"),
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import math
|
import math
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import time
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
|
||||||
import mujoco
|
import mujoco
|
||||||
@@ -179,6 +180,8 @@ class DualArmSimulator(Node):
|
|||||||
def close_viewer(self) -> None:
|
def close_viewer(self) -> None:
|
||||||
if self._viewer is not None:
|
if self._viewer is not None:
|
||||||
self._viewer.close()
|
self._viewer.close()
|
||||||
|
# MuJoCo 在后台 daemon 线程释放 GLX;立即退出解释器会触发段错误。
|
||||||
|
time.sleep(0.1)
|
||||||
self._viewer = None
|
self._viewer = None
|
||||||
|
|
||||||
|
|
||||||
@@ -188,6 +191,8 @@ def main(args=None) -> None:
|
|||||||
try:
|
try:
|
||||||
node = DualArmSimulator()
|
node = DualArmSimulator()
|
||||||
rclpy.spin(node)
|
rclpy.spin(node)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
if node is not None:
|
if node is not None:
|
||||||
node.close_viewer()
|
node.close_viewer()
|
||||||
|
|||||||
Reference in New Issue
Block a user