fix(input/gestures): enforce numpy array immutability in HandLandmarks; add CameraCapture tests
- Set points.flags.writeable = False in HandsDetector.detect() so in-place mutation of HandLandmarks.points raises ValueError (frozen=True alone does not protect numpy array contents) - Extend test_handlandmarks_is_immutable to assert ValueError on array mutation - Add test_camera.py with 3 tests covering is_open, frames() yield/break behaviour, and context manager release (was at 0% coverage) - Remove unused `import numpy as np` from camera.py; fix frames() return annotation to Iterator (np.ndarray ref removed with the import)
This commit is contained in:
parent
cb3d186a58
commit
0f5ea86ab0
4 changed files with 57 additions and 3 deletions
|
|
@ -10,7 +10,6 @@ from __future__ import annotations
|
|||
from typing import Iterator
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
|
||||
class CameraCapture:
|
||||
|
|
@ -39,7 +38,7 @@ class CameraCapture:
|
|||
def is_open(self) -> bool:
|
||||
return self._cap.isOpened()
|
||||
|
||||
def frames(self) -> Iterator[np.ndarray]:
|
||||
def frames(self) -> Iterator:
|
||||
"""Yield BGR uint8 frames until camera fails or caller breaks."""
|
||||
while self._cap.isOpened():
|
||||
ok, frame = self._cap.read()
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ class HandsDetector:
|
|||
out: list[HandLandmarks] = []
|
||||
for lm, hand in zip(results.multi_hand_landmarks, results.multi_handedness):
|
||||
points = np.array([[p.x, p.y, p.z] for p in lm.landmark], dtype=np.float32)
|
||||
points.flags.writeable = False # enforce immutability of stored array
|
||||
out.append(
|
||||
HandLandmarks(
|
||||
points=points,
|
||||
|
|
|
|||
48
tests/test_input/test_gestures/test_camera.py
Normal file
48
tests/test_input/test_gestures/test_camera.py
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import numpy as np
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
@patch("circuitforge_core.input.gestures.camera.cv2")
|
||||
def test_is_open_reflects_videocapture_state(mock_cv2):
|
||||
from circuitforge_core.input.gestures.camera import CameraCapture
|
||||
|
||||
mock_cv2.VideoCapture.return_value.isOpened.return_value = True
|
||||
cam = CameraCapture()
|
||||
assert cam.is_open is True
|
||||
|
||||
mock_cv2.VideoCapture.return_value.isOpened.return_value = False
|
||||
cam2 = CameraCapture()
|
||||
assert cam2.is_open is False
|
||||
|
||||
|
||||
@patch("circuitforge_core.input.gestures.camera.cv2")
|
||||
def test_frames_yields_until_read_fails(mock_cv2):
|
||||
from circuitforge_core.input.gestures.camera import CameraCapture
|
||||
|
||||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
mock_cap = MagicMock()
|
||||
mock_cap.isOpened.return_value = True
|
||||
mock_cap.read.side_effect = [
|
||||
(True, frame),
|
||||
(True, frame),
|
||||
(False, None), # triggers break
|
||||
]
|
||||
mock_cv2.VideoCapture.return_value = mock_cap
|
||||
|
||||
cam = CameraCapture()
|
||||
collected = list(cam.frames())
|
||||
assert len(collected) == 2
|
||||
|
||||
|
||||
@patch("circuitforge_core.input.gestures.camera.cv2")
|
||||
def test_context_manager_calls_release(mock_cv2):
|
||||
from circuitforge_core.input.gestures.camera import CameraCapture
|
||||
|
||||
mock_cap = MagicMock()
|
||||
mock_cv2.VideoCapture.return_value = mock_cap
|
||||
|
||||
with CameraCapture() as cam:
|
||||
pass
|
||||
|
||||
mock_cap.release.assert_called_once()
|
||||
|
|
@ -79,7 +79,13 @@ def test_handlandmarks_is_immutable(mock_mp):
|
|||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
result = detector.detect(frame)[0]
|
||||
with pytest.raises((AttributeError, TypeError)):
|
||||
result.handedness = "Left" # frozen dataclass must reject mutation
|
||||
result.handedness = (
|
||||
"Left" # frozen dataclass must reject attribute reassignment
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
result.points[0] = np.array(
|
||||
[1.0, 2.0, 3.0]
|
||||
) # writeable=False must reject in-place mutation
|
||||
|
||||
|
||||
@patch("circuitforge_core.input.gestures.hands.mp")
|
||||
|
|
|
|||
Loading…
Reference in a new issue