156 lines
7.4 KiB
Python
156 lines
7.4 KiB
Python
"""Latest-result ownership, optional output, and asynchronous consumer contract."""
|
|
import copy
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from unittest.mock import patch
|
|
from urllib.request import urlopen
|
|
import numpy as np
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
import run_camera as app
|
|
from latest_result import LatestResult, result_to_dict
|
|
|
|
|
|
class ResultTests(unittest.TestCase):
|
|
def test_read_only_owned_snapshot_and_old_reference(self):
|
|
channel=LatestResult()
|
|
self.assertIsNone(channel.load())
|
|
source={'frame_index':0,'risk':{'risk_score':None},'boxes':[[1,2,3,4]]}
|
|
channel.publish(source);old=channel.load()
|
|
source['boxes'][0][0]=99
|
|
self.assertEqual(old['boxes'][0][0],1)
|
|
with self.assertRaises(TypeError):old['risk']['risk_score']=0
|
|
with self.assertRaises(TypeError):old['boxes'][0][0]=0
|
|
channel.publish({'frame_index':1,'risk':{'risk_score':1}})
|
|
self.assertEqual(old['frame_index'],0)
|
|
self.assertIs(channel.load(),channel.load())
|
|
self.assertEqual(channel.load()['frame_index'],1)
|
|
self.assertEqual(result_to_dict(old)['boxes'],[[1,2,3,4]])
|
|
|
|
def test_wait_replaces_old_frames_and_close_wakes_reader(self):
|
|
channel=LatestResult()
|
|
for i in range(20):channel.publish({'frame_index':i})
|
|
self.assertEqual(channel.wait(0,.1)['frame_index'],19)
|
|
self.assertIsNone(channel.wait(19,.01))
|
|
answer=[];worker=threading.Thread(target=lambda:answer.append(channel.wait(19,1)))
|
|
worker.start();channel.close();worker.join(1)
|
|
self.assertFalse(worker.is_alive());self.assertEqual(answer,[None])
|
|
self.assertEqual(channel.load()['frame_index'],19)
|
|
|
|
|
|
class FakeNative:
|
|
instances=[]
|
|
def __init__(self,args):
|
|
self.args=args;self.index=0;self.closed=False;self.opened=False
|
|
self.frame=np.zeros((args.height,args.width,3),np.uint8)
|
|
self.__class__.instances.append(self)
|
|
def open_camera(self):self.opened=True
|
|
def read(self):
|
|
time.sleep(.005)
|
|
self.index+=1
|
|
return self.frame,{'sequence':self.index,'capture_monotonic_ms':time.monotonic()*1000,'dropped_before_inference':0}
|
|
def can_snapshot(self):return {'positions':[-1]*4,'age_ms':-1}
|
|
def close(self):self.closed=True
|
|
|
|
|
|
class FakePose:
|
|
def __init__(self,*args):pass
|
|
def infer(self,frame,index):
|
|
return {'track_ids':[7],'boxes':[[0,0,frame.shape[1],frame.shape[0]]],
|
|
'temporal_selected_keypoints':[[[20.,20.]]*133],
|
|
'temporal_metadata':[{'left':{},'right':{}}],'global_hands':[]}
|
|
|
|
|
|
class PipelineTests(unittest.TestCase):
|
|
def run_fake(self,output,*extra):
|
|
args=app.parse_args(['--output',str(output),'--max-frames','3',*extra])
|
|
pipeline=app.CameraPipeline(args)
|
|
with patch.object(app,'NativeBackend',FakeNative),patch.object(app,'NativePoseEngine',FakePose):
|
|
pipeline.start();pipeline.join(10)
|
|
self.assertFalse(pipeline.running())
|
|
self.assertTrue(FakeNative.instances[-1].closed)
|
|
return pipeline
|
|
|
|
def test_default_has_no_renderer_http_encoding_or_disk_output(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
out=Path(tmp)/'must-not-exist'
|
|
with patch.object(app,'Preview') as preview,patch.object(app,'render_frame') as render,patch.object(app.cv2,'imencode') as encode:
|
|
pipeline=self.run_fake(out)
|
|
preview.assert_not_called();render.assert_not_called();encode.assert_not_called()
|
|
self.assertFalse(out.exists())
|
|
result=pipeline.latest_result
|
|
self.assertEqual(result['frame_index'],2)
|
|
self.assertIs(result,pipeline.latest())
|
|
self.assertEqual(result['risk']['risk_type'],'CAN_UNKNOWN')
|
|
self.assertIsNone(result['risk']['risk_score'])
|
|
self.assertEqual(len(result['skeletons'][0]['keypoints']),133)
|
|
self.assertEqual(pipeline.summary['frames'],3)
|
|
with self.assertRaises(RuntimeError):pipeline.start()
|
|
|
|
def test_json_output_matches_in_memory_result_without_rendering(self):
|
|
with tempfile.TemporaryDirectory() as tmp,patch.object(app,'render_frame') as render:
|
|
out=Path(tmp)/'json'
|
|
pipeline=self.run_fake(out,'--save-results')
|
|
disk=[json.loads(line) for line in (out/'causal_results.jsonl').read_text().splitlines()]
|
|
self.assertEqual(disk[-1],result_to_dict(pipeline.latest_result))
|
|
self.assertTrue((out/'latest.json').exists());self.assertTrue((out/'summary.json').exists())
|
|
self.assertFalse((out/'latest.jpg').exists());render.assert_not_called()
|
|
|
|
def test_preview_start_failure_does_not_stop_inference(self):
|
|
with tempfile.TemporaryDirectory() as tmp,patch.object(app,'Preview',side_effect=OSError('port busy')):
|
|
pipeline=self.run_fake(Path(tmp)/'none','--visualize')
|
|
self.assertEqual(pipeline.latest_result['frame_index'],2)
|
|
|
|
def test_worker_error_reaches_downstream_and_releases_resources(self):
|
|
with tempfile.TemporaryDirectory() as tmp,patch.object(FakePose,'infer',side_effect=RuntimeError('pose failed')):
|
|
with self.assertRaisesRegex(RuntimeError,'pose failed'):self.run_fake(Path(tmp)/'none')
|
|
self.assertTrue(FakeNative.instances[-1].closed)
|
|
|
|
def test_consumer_reads_while_running_and_can_request_stop(self):
|
|
args=app.parse_args([]);pipeline=app.CameraPipeline(args)
|
|
with patch.object(app,'NativeBackend',FakeNative),patch.object(app,'NativePoseEngine',FakePose):
|
|
pipeline.start()
|
|
try:
|
|
first=pipeline.wait_for_result(timeout=5)
|
|
self.assertIsNotNone(first)
|
|
second=pipeline.wait_for_result(first['frame_index'],5)
|
|
self.assertGreater(second['frame_index'],first['frame_index'])
|
|
self.assertTrue(pipeline.running())
|
|
finally:
|
|
pipeline.request_stop();pipeline.join(5)
|
|
self.assertFalse(pipeline.running())
|
|
|
|
|
|
class PreviewTests(unittest.TestCase):
|
|
def test_http_results_follow_memory_without_waiting_for_new_image(self):
|
|
results=LatestResult();results.publish({'frame_index':1,'risk':{'risk_score':None}})
|
|
preview=app.Preview('127.0.0.1',0,results)
|
|
try:
|
|
url='http://127.0.0.1:%d/result.json'%preview.server.server_port
|
|
self.assertEqual(json.load(urlopen(url,timeout=2))['frame_index'],1)
|
|
results.publish({'frame_index':2,'risk':{'risk_score':None}})
|
|
self.assertEqual(json.load(urlopen(url,timeout=2))['frame_index'],2)
|
|
finally:preview.close()
|
|
|
|
def test_preview_renders_on_observer_thread(self):
|
|
results=LatestResult();rendered=threading.Event();worker_threads=[]
|
|
def render(*args):
|
|
worker_threads.append(threading.current_thread().name)
|
|
return np.zeros((48,64,3),np.uint8)
|
|
preview=app.Preview('127.0.0.1',0,results)
|
|
try:
|
|
with patch.object(app,'render_output',side_effect=render),patch.object(preview,'update',side_effect=lambda image:rendered.set()):
|
|
preview.submit(None,None,None,None,None,None)
|
|
self.assertTrue(rendered.wait(2))
|
|
self.assertEqual(worker_threads,['risk-preview'])
|
|
finally:preview.close()
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|