Files
vision-anti-pinch/tests/test_latest_result.py
T

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