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
|
from typing import Iterator
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
class CameraCapture:
|
class CameraCapture:
|
||||||
|
|
@ -39,7 +38,7 @@ class CameraCapture:
|
||||||
def is_open(self) -> bool:
|
def is_open(self) -> bool:
|
||||||
return self._cap.isOpened()
|
return self._cap.isOpened()
|
||||||
|
|
||||||
def frames(self) -> Iterator[np.ndarray]:
|
def frames(self) -> Iterator:
|
||||||
"""Yield BGR uint8 frames until camera fails or caller breaks."""
|
"""Yield BGR uint8 frames until camera fails or caller breaks."""
|
||||||
while self._cap.isOpened():
|
while self._cap.isOpened():
|
||||||
ok, frame = self._cap.read()
|
ok, frame = self._cap.read()
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ class HandsDetector:
|
||||||
out: list[HandLandmarks] = []
|
out: list[HandLandmarks] = []
|
||||||
for lm, hand in zip(results.multi_hand_landmarks, results.multi_handedness):
|
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 = 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(
|
out.append(
|
||||||
HandLandmarks(
|
HandLandmarks(
|
||||||
points=points,
|
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)
|
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||||
result = detector.detect(frame)[0]
|
result = detector.detect(frame)[0]
|
||||||
with pytest.raises((AttributeError, TypeError)):
|
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")
|
@patch("circuitforge_core.input.gestures.hands.mp")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue