bookmark/tests/test_camera.py
pyr0ball efe3f65cd5 fix: release camera capture handle on failed or repeated open
- Fix critical resource leak when cv2.VideoCapture.isOpened() returns False:
  create capture in local variable, verify it opened, call .release() before
  raising RuntimeError, only assign to self._capture after confirming success
- Fix potential leak on repeated open(): release any existing self._capture
  before creating a new one
- Add regression test: test_open_raises_when_capture_not_opened now verifies
  .release() is called on failed capture
- Add new test: test_open_twice_releases_first_capture verifies first capture's
  .release() is called before second is assigned

All 28 tests passing (5 camera + 23 existing).
2026-07-13 10:44:47 -07:00

77 lines
2.6 KiB
Python

from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from bookmark.camera import CameraSource
def test_open_raises_when_capture_not_opened():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = False
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
with pytest.raises(RuntimeError, match="Could not open camera"):
source.open()
# Verify that release() was called on the failed capture
mock_instance.release.assert_called_once()
def test_read_frame_returns_frame_from_capture():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = True
fake_frame = np.zeros((10, 10, 3), dtype=np.uint8)
mock_instance.read.return_value = (True, fake_frame)
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
source.open()
frame = source.read_frame()
assert frame.shape == (10, 10, 3)
def test_read_frame_raises_when_capture_fails():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
mock_instance = MagicMock()
mock_instance.isOpened.return_value = True
mock_instance.read.return_value = (False, None)
mock_video_capture.return_value = mock_instance
source = CameraSource(device_index=0)
source.open()
with pytest.raises(RuntimeError, match="Failed to read frame"):
source.read_frame()
def test_read_frame_raises_if_not_opened():
source = CameraSource(device_index=0)
with pytest.raises(RuntimeError, match="not opened"):
source.read_frame()
def test_open_twice_releases_first_capture():
with patch("bookmark.camera.cv2.VideoCapture") as mock_video_capture:
# Setup two distinct mock instances
first_mock = MagicMock()
first_mock.isOpened.return_value = True
second_mock = MagicMock()
second_mock.isOpened.return_value = True
mock_video_capture.side_effect = [first_mock, second_mock]
source = CameraSource(device_index=0)
# First open
source.open()
assert source._capture is first_mock
# Second open should release the first capture
source.open()
assert source._capture is second_mock
# Verify the first mock's release() was called before the second was assigned
first_mock.release.assert_called_once()