hopefully run tests
This commit is contained in:
@@ -24,6 +24,9 @@ jobs:
|
|||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
|
|
||||||
- name: Install PyInstaller
|
- name: Install PyInstaller
|
||||||
run: pip install pyinstaller
|
run: pip install pyinstaller
|
||||||
|
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"makefile.configureOnOpen": false
|
||||||
|
}
|
||||||
152
tests/test_main.py
Normal file
152
tests/test_main.py
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
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()
|
||||||
Reference in New Issue
Block a user