import threading
import unittest
from unittest.mock import patch, Mock
from xml.etree.ElementTree import fromstring
import ptz
def xml(body):
return fromstring('' + body + '')
class PresetTests(unittest.TestCase):
def setUp(self):
self.ctrl = ptz.Controller()
self.camera = dict(host='192.168.0.2', username='admin', password='example', mainPath='/stream1')
self.info = dict(endpoint=(80, '/onvif/PTZ'), profile='p1', node='shared')
self.snapshot = dict(revision='v1', maximum=12, items=[dict(token='1', name='Door')])
def test_read_presets_validates_device_and_list(self):
node = xml('12')
items = xml('Door')
with patch.object(self.ctrl, 'discover', return_value=self.info), patch.object(self.ctrl, 'call', side_effect=[node, items]):
result, _ = self.ctrl.read_presets(self.camera)
self.assertEqual(result['items'], self.snapshot['items'])
self.assertEqual(len(result['revision']), 64)
self.assertNotIn('endpoint', result)
for response in [xml('0'), xml('999999')]:
with patch.object(self.ctrl, 'discover', return_value=self.info), patch.object(self.ctrl, 'call', return_value=response):
with self.assertRaises(ValueError): self.ctrl.read_presets(self.camera)
with patch.object(self.ctrl, 'discover', return_value=self.info), patch.object(self.ctrl, 'call', side_effect=[node, xml('')]):
with self.assertRaises(ValueError): self.ctrl.read_presets(self.camera)
def command(self, data, latest=None, response=None):
versions = [(self.snapshot, self.info)]
if latest is not None: versions.append((latest, self.info))
with patch.object(self.ctrl, 'discover', return_value=self.info), patch.object(self.ctrl, 'read_presets', side_effect=versions), patch.object(self.ctrl, 'call', return_value=response) as call:
result = self.ctrl.preset_command(self.camera, dict(revision='v1', **data))
return result, call
def test_creation_escapes_name_and_confirms_returned_token(self):
latest = dict(self.snapshot, items=[*self.snapshot['items'], dict(token='2', name='Yard & Gate')])
result, call = self.command(dict(action='create', name='Yard & Gate'), latest, xml('2'))
self.assertTrue(result['verified'])
self.assertIn('Yard & Gate', call.call_args.args[4])
self.assertNotIn('', call.call_args.args[4])
def test_stale_snapshot_and_unknown_token_cannot_write(self):
for data in [dict(action='create', name='Yard', revision='old'), dict(action='goto', token='unknown', revision='v1')]:
with patch.object(self.ctrl, 'discover', return_value=self.info), patch.object(self.ctrl, 'read_presets', return_value=(self.snapshot,self.info)), patch.object(self.ctrl, 'call') as call:
with self.assertRaises(ValueError): self.ctrl.preset_command(self.camera, data)
call.assert_not_called()
self.assertFalse(self.ctrl.locks[self.ctrl.node_key(self.camera,self.info)].locked())
def test_invalid_names_duplicate_names_and_full_device_rejected(self):
for name in ['', 'x'*41, 'bad\nname', None, ' door ']:
with self.assertRaises(ValueError): self.command(dict(action='create', name=name))
self.snapshot['maximum']=1
with self.assertRaises(ValueError): self.command(dict(action='create',name='Yard'))
def test_remove_readback_and_unsupported_replace_rejected(self):
result,call=self.command(dict(action='remove',token='1'),dict(self.snapshot,items=[]))
self.assertTrue(result['verified']);self.assertEqual(call.call_args.args[3],'RemovePreset')
with patch.object(self.ctrl,'call') as call:
with self.assertRaises(ValueError):self.ctrl.preset_command(self.camera,dict(action='replace',token='1',name='Door'))
call.assert_not_called()
def test_ignored_write_is_not_reported_as_verified(self):
result,_=self.command(dict(action='create',name='Yard'),self.snapshot,xml('2'))
self.assertFalse(result['verified'])
def test_readback_failure_releases_lock_and_is_not_retried(self):
with patch.object(self.ctrl,'discover',return_value=self.info),patch.object(self.ctrl,'read_presets',side_effect=[(self.snapshot,self.info),OSError()]),patch.object(self.ctrl,'call') as call:
with self.assertRaises(OSError):self.ctrl.preset_command(self.camera,dict(action='remove',token='1',revision='v1'))
self.assertEqual(call.call_count,1)
self.assertFalse(self.ctrl.locks[self.ctrl.node_key(self.camera,self.info)].locked())
def test_goto_polls_device_and_always_stops(self):
status=xml('IDLE')
with patch.object(self.ctrl,'discover',return_value=self.info),patch.object(self.ctrl,'read_presets',return_value=(self.snapshot,self.info)),patch.object(self.ctrl,'call',side_effect=[None,status,None]) as call,patch('ptz.threading.Event',return_value=Mock(wait=Mock(return_value=False))):
result=self.ctrl.preset_command(self.camera,dict(action='goto',token='1',revision='v1'))
self.assertTrue(result['completed'])
self.assertEqual([c.args[3] for c in call.call_args_list],['GotoPreset','GetStatus','Stop'])
self.assertFalse(self.ctrl.cancellations)
def test_goto_network_failure_still_attempts_stop(self):
with patch.object(self.ctrl,'discover',return_value=self.info),patch.object(self.ctrl,'read_presets',return_value=(self.snapshot,self.info)),patch.object(self.ctrl,'call',side_effect=[OSError(),None]) as call:
with self.assertRaises(OSError):self.ctrl.preset_command(self.camera,dict(action='goto',token='1',revision='v1'))
self.assertEqual(call.call_args.args[3],'Stop');self.assertFalse(self.ctrl.cancellations)
def test_timeout_reports_incomplete_and_stops(self):
with patch.object(self.ctrl,'discover',return_value=self.info),patch.object(self.ctrl,'read_presets',return_value=(self.snapshot,self.info)),patch.object(self.ctrl,'call',return_value=xml('MOVING')) as call,patch('ptz.time.monotonic',side_effect=[0,0,11]),patch('ptz.threading.Event',return_value=Mock(wait=Mock(return_value=False))):
result=self.ctrl.preset_command(self.camera,dict(action='goto',token='1',revision='v1'))
self.assertFalse(result['completed']);self.assertEqual(call.call_args.args[3],'Stop')
def test_shared_node_blocks_preset_writes_but_stop_cancels(self):
key=self.ctrl.node_key(self.camera,self.info);lock=threading.Lock();lock.acquire();self.ctrl.locks[key]=lock
event=threading.Event();self.ctrl.cancellations[key]=event
with patch.object(self.ctrl,'discover',return_value=self.info),patch.object(self.ctrl,'call'):
with self.assertRaises(ValueError):self.ctrl.preset_command(self.camera,dict(action='create',name='Yard'))
self.ctrl.command(self.camera,'stop');self.assertTrue(event.is_set())
lock.release()
if __name__=='__main__':unittest.main()