Files
mucapy/tests/test_main.py
2025-06-03 18:19:44 +02:00

152 lines
6.2 KiB
Python

import os
import sys
import json
import tempfile
import shutil
import unittest
from unittest.mock import patch, MagicMock
import numpy as np
# Import the classes to test
from mucapy.main import Config, CameraThread, MultiCamYOLODetector
class TestConfig(unittest.TestCase):
def setUp(self):
# Create a temporary directory for config
self.temp_dir = tempfile.mkdtemp()
self.config_file = os.path.join(self.temp_dir, 'config.json')
self.expanduser_patcher = patch('os.path.expanduser', return_value=self.temp_dir)
self.makedirs_patcher = patch('os.makedirs', return_value=None)
self.expanduser_patcher.start()
self.makedirs_patcher.start()
# Patch environment variables that may affect config path
self.env_patches = []
for var in ['APPDATA', 'USERPROFILE']:
self.env_patches.append(patch.dict(os.environ, {var: self.temp_dir}))
self.env_patches[-1].start()
self.config = Config()
self.config.config_file = self.config_file
def tearDown(self):
self.expanduser_patcher.stop()
self.makedirs_patcher.stop()
for p in self.env_patches:
p.stop()
shutil.rmtree(self.temp_dir)
def test_save_and_load_setting(self):
self.config.save_setting('test_key', 'test_value')
self.assertEqual(self.config.load_setting('test_key'), 'test_value')
# Reload config to check persistence
self.config.settings['test_key'] = None
self.config.load_config()
self.assertEqual(self.config.load_setting('test_key'), 'test_value')
def test_default_settings(self):
self.assertIn('network_cameras', self.config.settings)
self.assertIn('last_model_dir', self.config.settings)
def test_load_nonexistent_key(self):
self.assertIsNone(self.config.load_setting('nonexistent_key'))
self.assertEqual(self.config.load_setting('nonexistent_key', default=123), 123)
def test_save_invalid_json(self):
# Simulate corrupted config file
with open(self.config_file, 'w') as f:
f.write("{invalid json")
# Should not raise, should print error and keep defaults
self.config.load_config()
self.assertIn('network_cameras', self.config.settings)
class TestCameraThread(unittest.TestCase):
def setUp(self):
self.thread = CameraThread(0, {'url': 'http://192.168.1.2:4747'})
@patch('mucapy.main.CameraThread.validate_url')
def test_validate_url(self, mock_validate_url):
mock_validate_url.side_effect = lambda url: 'http://192.168.1.2:4747/video' if ':4747' in url else url
url = '192.168.1.2:4747'
validated = self.thread.validate_url(url)
self.assertEqual(validated, 'http://192.168.1.2:4747/video')
url2 = 'http://example.com/stream'
validated2 = self.thread.validate_url(url2)
self.assertEqual(validated2, url2)
def test_construct_camera_url_no_auth(self):
info = {'url': 'http://example.com/stream'}
url = self.thread.construct_camera_url(info)
self.assertEqual(url, 'http://example.com/stream')
def test_construct_camera_url_with_auth(self):
info = {'url': 'http://example.com/stream', 'username': 'user', 'password': 'pass'}
url = self.thread.construct_camera_url(info)
self.assertTrue(url.startswith('http://user:pass@'))
def test_construct_camera_url_invalid_url(self):
# Should handle invalid URL gracefully
info = {'url': '!!!not_a_url'}
url = self.thread.construct_camera_url(info)
self.assertIsInstance(url, str) # Should still return a string, possibly normalized
def test_construct_camera_url_non_dict(self):
url = self.thread.construct_camera_url('http://example.com/stream')
self.assertEqual(url, 'http://example.com/stream')
class TestMultiCamYOLODetector(unittest.TestCase):
def setUp(self):
self.detector = MultiCamYOLODetector()
def test_add_and_remove_network_camera(self):
self.detector.add_network_camera('testcam', 'http://test')
self.assertIn('testcam', self.detector.network_cameras)
self.detector.remove_network_camera('testcam')
self.assertNotIn('testcam', self.detector.network_cameras)
@patch('os.listdir', side_effect=FileNotFoundError)
def test_load_yolo_model_dir_not_found(self, mock_listdir):
result = self.detector.load_yolo_model('/nonexistent')
self.assertFalse(result)
@patch('os.listdir', return_value=['model.weights', 'model.cfg', 'model.names'])
@patch('cv2.dnn.readNet', side_effect=Exception("Failed to load net"))
def test_load_yolo_model_readnet_exception(self, mock_readnet, mock_listdir):
result = self.detector.load_yolo_model('/some/dir')
self.assertFalse(result)
@patch('os.path.exists', return_value=False)
@patch('cv2.dnn.readNet', return_value=MagicMock())
@patch('os.listdir', return_value=[])
def test_load_yolo_model_no_files(self, mock_listdir, mock_readnet, mock_exists):
result = self.detector.load_yolo_model('/nonexistent')
self.assertFalse(result)
@patch('cv2.VideoCapture')
def test_scan_for_cameras(self, mock_vc):
# Simulate one camera available
mock_instance = MagicMock()
mock_instance.isOpened.return_value = True
mock_vc.return_value = mock_instance
cams = self.detector.scan_for_cameras(max_to_check=1)
self.assertTrue(any(c.isdigit() for c in cams))
@patch('cv2.VideoCapture')
def test_scan_for_cameras_none_available(self, mock_vc):
# Simulate no cameras available
mock_instance = MagicMock()
mock_instance.isOpened.return_value = False
mock_vc.return_value = mock_instance
cams = self.detector.scan_for_cameras(max_to_check=1)
self.assertFalse(any(c.isdigit() for c in cams))
def test_connect_cameras_empty(self):
# Should not fail if given empty list
result = self.detector.connect_cameras([])
self.assertFalse(result)
def test_disconnect_cameras_noop(self):
# Should not raise if no cameras connected
self.detector.disconnect_cameras() # Should not raise
if __name__ == '__main__':
unittest.main()