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()