Files
ucvl-home-vision/test_ptz_presets.py
T

105 lines
7.4 KiB
Python
Raw Permalink Normal View History

import threading
import unittest
from unittest.mock import patch, Mock
from xml.etree.ElementTree import fromstring
import ptz
def xml(body):
return fromstring('<root xmlns:t="' + ptz.onvif.SCHEMA + '" xmlns:p="' + ptz.PTZ + '">' + body + '</root>')
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('<t:MaximumNumberOfPresets>12</t:MaximumNumberOfPresets>')
items = xml('<p:Preset token="1"><t:Name>Door</t:Name></p:Preset>')
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('<t:MaximumNumberOfPresets>0</t:MaximumNumberOfPresets>'), xml('<t:MaximumNumberOfPresets>999999</t:MaximumNumberOfPresets>')]:
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('<p:Preset token="1"/><p:Preset token="1"/>')]):
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('<p:PresetToken>2</p:PresetToken>'))
self.assertTrue(result['verified'])
self.assertIn('Yard &amp; Gate', call.call_args.args[4])
self.assertNotIn('<m:PresetToken>', 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('<p:PresetToken>2</p:PresetToken>'))
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('<t:MoveStatus><t:PanTilt>IDLE</t:PanTilt></t:MoveStatus>')
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('<t:MoveStatus><t:PanTilt>MOVING</t:PanTilt></t:MoveStatus>')) 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()