From 0dfe3d77dce4ef2c896d261236bee43cb3608797 Mon Sep 17 00:00:00 2001 From: YikaiFu-cart Date: Tue, 4 Aug 2026 13:45:44 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=89=E5=85=A8=E5=85=B3=E9=97=AD=20M?= =?UTF-8?q?uJoCo=20=E6=98=BE=E7=A4=BA=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xr_rm_mujoco/test/test_dual_arm_simulator.py | 31 +++++++++++++++++++ .../xr_rm_mujoco/dual_arm_simulator.py | 5 +++ 2 files changed, 36 insertions(+) diff --git a/xr_rm_mujoco/test/test_dual_arm_simulator.py b/xr_rm_mujoco/test/test_dual_arm_simulator.py index 79999d0..8e27dd2 100644 --- a/xr_rm_mujoco/test/test_dual_arm_simulator.py +++ b/xr_rm_mujoco/test/test_dual_arm_simulator.py @@ -1,9 +1,12 @@ import math from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock import pytest import yaml +from xr_rm_mujoco import dual_arm_simulator as simulator_module from xr_rm_mujoco.dual_arm_simulator import ( ARM_JOINT_NAMES, DualArmKinematicModel, @@ -70,6 +73,34 @@ def test_mujoco_config_contains_only_render_parameters() -> None: 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( ("names", "positions", "match"), [ diff --git a/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py b/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py index cc28e59..893b0e9 100644 --- a/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py +++ b/xr_rm_mujoco/xr_rm_mujoco/dual_arm_simulator.py @@ -4,6 +4,7 @@ from __future__ import annotations import math from pathlib import Path +import time from typing import Callable import mujoco @@ -179,6 +180,8 @@ class DualArmSimulator(Node): def close_viewer(self) -> None: if self._viewer is not None: self._viewer.close() + # MuJoCo 在后台 daemon 线程释放 GLX;立即退出解释器会触发段错误。 + time.sleep(0.1) self._viewer = None @@ -188,6 +191,8 @@ def main(args=None) -> None: try: node = DualArmSimulator() rclpy.spin(node) + except KeyboardInterrupt: + pass finally: if node is not None: node.close_viewer()