from __future__ import annotations from app.exercises.dead_bug.state_machine import DeadBugStateMachine from app.exercises.dead_bug.types import DeadBugMetrics, DeadBugPhase class TestDeadBugStateMachine: """死虫式状态机单元测试""" def _ready_metrics(self) -> DeadBugMetrics: """构建准备姿态的度量数据""" return DeadBugMetrics( left_arm_extended=False, right_arm_extended=False, left_leg_extended=False, right_leg_extended=False, left_elbow_angle=90, right_elbow_angle=90, left_knee_angle=100, right_knee_angle=100, feedback=[], ) def _extended_left(self) -> DeadBugMetrics: """构建左臂+右腿对角伸展的度量数据""" return DeadBugMetrics( left_arm_extended=True, right_arm_extended=False, left_leg_extended=False, right_leg_extended=True, left_elbow_angle=160, right_elbow_angle=90, left_knee_angle=90, right_knee_angle=160, feedback=[], ) def _both_legs_extended(self) -> DeadBugMetrics: """构建双腿同时伸展的非标准姿态""" return DeadBugMetrics( left_arm_extended=True, right_arm_extended=True, left_leg_extended=True, right_leg_extended=True, left_elbow_angle=160, right_elbow_angle=160, left_knee_angle=160, right_knee_angle=160, feedback=[], ) def _arms_extended_ready_legs(self) -> DeadBugMetrics: """构建腿已收回但手臂未收回的姿态""" return DeadBugMetrics( left_arm_extended=True, right_arm_extended=True, left_leg_extended=False, right_leg_extended=False, left_elbow_angle=160, right_elbow_angle=160, left_knee_angle=100, right_knee_angle=100, feedback=[], ) def _right_knee_angle(self, angle: float) -> DeadBugMetrics: """构建右膝角连续变化但伸展布尔值尚未稳定的姿态""" return DeadBugMetrics( left_arm_extended=True, right_arm_extended=True, left_leg_extended=False, right_leg_extended=False, left_elbow_angle=160, right_elbow_angle=160, left_knee_angle=90, right_knee_angle=angle, feedback=[], ) def test_initial_state(self): """测试:状态机初始化后应为READY且计数为0""" sm = DeadBugStateMachine() assert sm.phase == DeadBugPhase.READY assert sm.rep_count == 0 def test_no_transition_in_ready(self): """测试:准备姿态下不触发状态转换""" sm = DeadBugStateMachine() result = sm.update(self._ready_metrics()) assert sm.phase == DeadBugPhase.READY assert result.rep_count == 0 def test_confirm_extension(self): """测试:连续确认帧数后从READY转换到EXTENDING""" sm = DeadBugStateMachine(extension_confirm_frames=2, reset_confirm_frames=2) sm.update(self._extended_left()) assert sm.phase == DeadBugPhase.READY sm.update(self._extended_left()) assert sm.phase == DeadBugPhase.EXTENDING def test_confirm_extension_from_knee_angle_trend(self): """测试:膝角连续上升时,不依赖单帧伸展布尔值也能确认伸展""" sm = DeadBugStateMachine(extension_confirm_frames=2, reset_confirm_frames=2) sm.update(self._right_knee_angle(100)) sm.update(self._right_knee_angle(130)) assert sm.phase == DeadBugPhase.READY sm.update(self._right_knee_angle(145)) sm.update(self._right_knee_angle(150)) assert sm.phase == DeadBugPhase.EXTENDING def test_full_rep_counts_once_after_strict_reset(self): """测试:确认伸展后,只有严格回到准备姿态才计一次""" sm = DeadBugStateMachine(extension_confirm_frames=2, reset_confirm_frames=2) sm.update(self._extended_left()) sm.update(self._extended_left()) assert sm.phase == DeadBugPhase.EXTENDING sm.update(self._arms_extended_ready_legs()) assert sm.rep_count == 0 assert sm.phase == DeadBugPhase.NEED_RESET sm.update(self._ready_metrics()) result = sm.update(self._ready_metrics()) assert result.rep_count == 1 assert sm.phase == DeadBugPhase.READY result = sm.update(self._ready_metrics()) assert result.rep_count == 1 def test_both_legs_do_not_start_rep(self): """测试:双腿同时伸展不进入计数流程""" sm = DeadBugStateMachine(extension_confirm_frames=2, reset_confirm_frames=2) sm.update(self._both_legs_extended()) result = sm.update(self._both_legs_extended()) assert result.rep_count == 0 assert sm.phase == DeadBugPhase.READY def test_no_pose_preserves_confirmed_rep_until_reset(self): """测试:已确认伸展后短暂丢姿态,回到准备位仍能完成计数""" sm = DeadBugStateMachine(extension_confirm_frames=2, reset_confirm_frames=2) sm.update(self._extended_left()) sm.update(self._extended_left()) assert sm.phase == DeadBugPhase.EXTENDING sm.mark_no_pose() sm.update(self._ready_metrics()) result = sm.update(self._ready_metrics()) assert result.rep_count == 1 assert sm.phase == DeadBugPhase.READY