Files
vision-anti-pinch/tests/camera_timeout_test.cpp
T

193 lines
8.0 KiB
C++

// Exercise the production Camera with simulated capture and no GPU or camera.
#include "camera.hpp"
#include "uyvy_converter.hpp"
#include <linux/videodev2.h>
#include <chrono>
#include <cstring>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#ifdef TEST_V4L2_CAPTURE
#include "v4l2_capture.h"
#else
#include <cstdarg>
#include <cerrno>
#include <poll.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#endif
namespace {
std::string events;
std::size_t cursor{};
char current{};
unsigned sequence{};
int timeoutMs{};
bool realWait{};
std::function<void()> onStop;
unsigned char pixels[8]{};
void check(bool condition,char const* message) {
if(!condition)throw std::runtime_error(message);
}
void script(std::string value) {events=std::move(value);cursor=0;}
char next() {
check(cursor<events.size(),"Camera requested an unexpected extra capture attempt");
return current=events[cursor++];
}
void expectTimeout(cms::Camera& camera) {
try {camera.read();} catch(std::runtime_error const& e) {
check(std::string(e.what()).find("6 consecutive times")!=std::string::npos,"Wrong timeout failure");
return;
}
throw std::runtime_error("Six consecutive timeouts did not fail");
}
}
namespace cms {
struct UyvyConverter::Impl {int width,height;};
UyvyConverter::UyvyConverter(int w,int h):impl_(std::make_unique<Impl>(Impl{w,h})){}
UyvyConverter::~UyvyConverter()=default;
std::shared_ptr<const BgrImage> UyvyConverter::convert(void const*,std::size_t) {
auto image=std::make_shared<BgrImage>();image->width=impl_->width;image->height=impl_->height;
image->stride=image->width*3;image->pixels.resize(image->stride*image->height);return image;
}
}
#ifdef TEST_V4L2_CAPTURE
V4l2Capture::~V4l2Capture()=default;
bool V4l2Capture::open(std::string const&){reconnect_needed_=false;return true;}
void V4l2Capture::close(){}
bool V4l2Capture::setFormat(uint32_t w,uint32_t h,uint32_t p){width_=w;height_=h;pixelformat_=p;return true;}
bool V4l2Capture::setFps(uint32_t){return true;}
bool V4l2Capture::initMmap(uint32_t){return true;}
bool V4l2Capture::startStreaming(){return true;}
void V4l2Capture::releaseFrame(Frame const&){}
bool V4l2Capture::getFrame(Frame& f,int waitMs) {
check(waitMs<=100,"Stop polling should remain at most 100 ms");
if(next()=='s'){onStop();return false;}
if(current=='d'){reconnect_needed_=true;return false;}
if(current=='e'||current=='x'){return false;}
if(current=='t') {
// Each scripted timeout represents a full logical capture window.
// The separate stop test exercises the short-wait cancellation path.
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));return false;
}
check(current=='f',"Unsupported mock V4L2 event");
f.data=pixels;f.size=sizeof(pixels);f.width=2;f.height=2;f.bytes_per_line=4;f.sequence=sequence++;
return true;
}
#else
extern "C" {
int __real_poll(pollfd*,nfds_t,int);
int __real_open(char const*,int,...);
int __real_close(int);
int __real_ioctl(int,unsigned long,...);
void* __real_mmap(void*,size_t,int,int,int,off_t);
int __real_munmap(void*,size_t);
int __wrap_open(char const* path,int flags,...) {
if(std::strcmp(path,"/mock/camera")==0)return 100000;
return __real_open(path,flags);
}
int __wrap_close(int fd){return fd==100000?0:__real_close(fd);}
void* __wrap_mmap(void* a,size_t n,int p,int f,int fd,off_t o) {
return fd==100000?pixels:__real_mmap(a,n,p,f,fd,o);
}
int __wrap_munmap(void* a,size_t n){return a==pixels?0:__real_munmap(a,n);}
int __wrap_ioctl(int fd,unsigned long request,...) {
va_list args;va_start(args,request);void* p=va_arg(args,void*);va_end(args);
if(fd!=100000)return __real_ioctl(fd,request,p);
switch(request) {
case VIDIOC_QUERYCAP:static_cast<v4l2_capability*>(p)->capabilities=V4L2_CAP_VIDEO_CAPTURE|V4L2_CAP_STREAMING;break;
case VIDIOC_S_FMT:static_cast<v4l2_format*>(p)->fmt.pix.bytesperline=4;break;
case VIDIOC_G_PARM:break;
case VIDIOC_REQBUFS:static_cast<v4l2_requestbuffers*>(p)->count=2;break;
case VIDIOC_QUERYBUF:static_cast<v4l2_buffer*>(p)->length=sizeof(pixels);break;
case VIDIOC_DQBUF: {
if(current=='x'){errno=EIO;return -1;}
auto& b=*static_cast<v4l2_buffer*>(p);b.index=0;b.bytesused=sizeof(pixels);b.sequence=sequence++;
b.flags=current=='e'?V4L2_BUF_FLAG_ERROR:0;break;
}
case VIDIOC_QBUF:case VIDIOC_STREAMON:case VIDIOC_STREAMOFF:break;
default:throw std::runtime_error("Unexpected camera ioctl");
}
return 0;
}
int __wrap_poll(pollfd* fds,nfds_t count,int waitMs) {
if(count==1)return __real_poll(fds,count,waitMs);
check(count==2&&waitMs>0&&waitMs<=timeoutMs,"Camera must respect the configured timeout window");
switch(next()) {
case 't':if(realWait)std::this_thread::sleep_for(std::chrono::milliseconds(waitMs));return 0;
case 'i':errno=EINTR;return -1;
case 's':onStop();return 0;
case 'd':fds[0].revents=POLLHUP;return 1;
case 'f':case 'e':case 'x':fds[0].revents=POLLIN;return 1;
default:throw std::runtime_error("Unexpected poll event");
}
}
int __wrap___poll_chk(pollfd* fds,nfds_t count,int waitMs,size_t size) {
check(size>=count*sizeof(pollfd),"Invalid fortified poll buffer size");
return __wrap_poll(fds,count,waitMs);
}
}
#endif
int main(int argc,char** argv)try {
cms::CameraConfig config;config.device="/mock/camera";config.width=config.height=2;config.buffers=2;
check(config.timeout_ms==2000,"Default per-attempt timeout must be 2 seconds");
realWait=argc>1&&std::string(argv[1])=="--wall-clock";
if(!realWait)config.timeout_ms=100;
timeoutMs=config.timeout_ms;
if(realWait) {
cms::Camera camera(config);script("tttttt");
auto before=std::chrono::steady_clock::now();expectTimeout(camera);
auto elapsed=std::chrono::duration<double>(std::chrono::steady_clock::now()-before).count();
check(cursor==6&&elapsed>=12.0&&elapsed<15.0,"Default timeout should fail after six attempts, about 12 seconds");
std::cout<<"PASS wall-clock six-timeout duration="<<elapsed<<" s\n";
return 0;
}
{
cms::Camera camera(config);
script("tttttf");check(bool(camera.read()),"Five timeouts should recover on a valid frame");
script("tttttf");check(bool(camera.read()),"A valid frame must reset the timeout count");
script("tttttt");
expectTimeout(camera);
check(cursor==6,"Failure must occur on the sixth timeout");
}
{
cms::Camera camera(config);onStop=[&]{camera.requestStop();};
script("tts");check(!camera.read(),"Stop must cancel retries without throwing");
check(cursor==3,"Stop should not wait for all six timeouts");onStop={};
}
{
cms::Camera camera(config);onStop=[&]{
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
camera.requestStop();
};
script("ttttts");check(!camera.read(),"Stop on the final timeout must take precedence over failure");
onStop={};
}
#ifndef TEST_V4L2_CAPTURE
{
cms::Camera camera(config);script("tttttif");
check(bool(camera.read()),"EINTR must not count as a timeout");
script("tttttet");expectTimeout(camera);
check(cursor==7,"A corrupt frame must not reset the timeout count");
}
{
cms::Camera camera(config);script("df");
check(bool(camera.read()),"Capture must reopen after a transient disconnection");
}
{
cms::Camera camera(config);script("xxf");
check(bool(camera.read()),"Transient EIO must recover without restarting the pipeline");
}
{
auto slow=config;slow.timeout_ms=2000;timeoutMs=slow.timeout_ms;
cms::Camera camera(slow);script(std::string(35,'e')+"f");
check(bool(camera.read()),"More than one second's frame count of corrupt frames must remain recoverable");
timeoutMs=config.timeout_ms;
}
#endif
std::cout<<"PASS camera timeout retries, recovery reset, sixth-timeout failure and cancellation\n";
return 0;
}catch(std::exception const& e){std::cerr<<"FAIL: "<<e.what()<<'\n';return 1;}