167 lines
4.9 KiB
C++
167 lines
4.9 KiB
C++
#include "fgc/CompositeCameraSource.h"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <stdexcept>
|
|
#include <vector>
|
|
|
|
using namespace fgc;
|
|
|
|
namespace {
|
|
|
|
// A source of `count` cameras whose trigger() delivers one frame per camera,
|
|
// stamped `ts`, when `ready` - and reports nothing otherwise.
|
|
struct FakeSource : ICameraSource {
|
|
int count;
|
|
long long ts;
|
|
bool ready = true;
|
|
bool throws_on_open = false;
|
|
int triggers = 0;
|
|
bool started = false;
|
|
std::string why_offline;
|
|
FrameCallback cb;
|
|
|
|
FakeSource(int n, long long t) : count(n), ts(t) {}
|
|
|
|
void open() override {
|
|
if (throws_on_open) throw std::runtime_error("no camera");
|
|
}
|
|
void close() override {}
|
|
void start() override { started = true; }
|
|
void stop() override { started = false; }
|
|
bool trigger() override {
|
|
++triggers;
|
|
if (!ready) return false;
|
|
for (int i = 0; i < count; ++i) {
|
|
Frame f;
|
|
f.cam_id = i;
|
|
f.timestamp_ms = ts + i;
|
|
cb(f);
|
|
}
|
|
return true;
|
|
}
|
|
void setFrameCallback(FrameCallback c) override { cb = std::move(c); }
|
|
int cameraCount() const override { return count; }
|
|
bool readFeature(int i, const std::string& name, double& v) override {
|
|
v = i * 100 + ts;
|
|
return name == "X";
|
|
}
|
|
std::string offline(int) override { return why_offline; }
|
|
std::vector<CameraDeviceInfo> deviceInfo() override {
|
|
std::vector<CameraDeviceInfo> out(count);
|
|
for (int i = 0; i < count; ++i) out[i].id = std::to_string(ts) + ":" + std::to_string(i);
|
|
return out;
|
|
}
|
|
bool grabFrame(int i, Frame& out, int) override {
|
|
out = Frame{};
|
|
out.cam_id = i;
|
|
out.timestamp_ms = ts + 50;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct Rig {
|
|
FakeSource* colour = nullptr;
|
|
FakeSource* thermal = nullptr;
|
|
std::unique_ptr<CompositeCameraSource> src;
|
|
std::vector<Frame> frames;
|
|
|
|
explicit Rig(int colour_count = 1) {
|
|
auto c = std::make_unique<FakeSource>(colour_count, 1000);
|
|
auto t = std::make_unique<FakeSource>(1, 5000);
|
|
colour = c.get();
|
|
thermal = t.get();
|
|
std::vector<std::unique_ptr<ICameraSource>> v;
|
|
v.push_back(std::move(c));
|
|
v.push_back(std::move(t));
|
|
src = std::make_unique<CompositeCameraSource>(std::move(v));
|
|
src->setFrameCallback([this](const Frame& f) { frames.push_back(f); });
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("the composite numbers the cameras leader first, and counts them all") {
|
|
Rig r(2);
|
|
CHECK(r.src->cameraCount() == 3);
|
|
REQUIRE(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 3);
|
|
CHECK(r.frames[0].cam_id == 0);
|
|
CHECK(r.frames[1].cam_id == 1);
|
|
CHECK(r.frames[2].cam_id == 2); // the thermal camera
|
|
}
|
|
|
|
TEST_CASE("one trigger gives every frame the leader's time") {
|
|
Rig r;
|
|
REQUIRE(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 2);
|
|
CHECK(r.frames[0].timestamp_ms == 1000);
|
|
CHECK(r.frames[1].timestamp_ms == 1000); // not its own 5000
|
|
}
|
|
|
|
TEST_CASE("the followers are asked only once the leader has delivered") {
|
|
Rig r;
|
|
r.colour->ready = false;
|
|
CHECK_FALSE(r.src->trigger());
|
|
CHECK_FALSE(r.src->trigger());
|
|
CHECK(r.thermal->triggers == 0); // no thermal picture without its colour one
|
|
CHECK(r.frames.empty());
|
|
r.colour->ready = true;
|
|
CHECK(r.src->trigger());
|
|
CHECK(r.thermal->triggers == 1);
|
|
}
|
|
|
|
TEST_CASE("a follower with nothing to give costs only its own picture") {
|
|
Rig r;
|
|
r.thermal->ready = false;
|
|
CHECK(r.src->trigger());
|
|
REQUIRE(r.frames.size() == 1);
|
|
CHECK(r.frames[0].cam_id == 0);
|
|
}
|
|
|
|
TEST_CASE("calls about one camera reach the source that has it") {
|
|
Rig r(2);
|
|
double v = 0;
|
|
REQUIRE(r.src->readFeature(1, "X", v));
|
|
CHECK(v == 1100); // colour source, its camera 1
|
|
REQUIRE(r.src->readFeature(2, "X", v));
|
|
CHECK(v == 5000); // thermal source, its camera 0
|
|
CHECK_FALSE(r.src->readFeature(3, "X", v));
|
|
|
|
r.thermal->why_offline = "unplugged";
|
|
CHECK(r.src->offline(0).empty());
|
|
CHECK(r.src->offline(2) == "unplugged");
|
|
|
|
const auto d = r.src->deviceInfo();
|
|
REQUIRE(d.size() == 3);
|
|
CHECK(d[2].id == "5000:0");
|
|
|
|
// A grab outside a trigger keeps its own time, under the global index.
|
|
Frame f;
|
|
REQUIRE(r.src->grabFrame(2, f, 10));
|
|
CHECK(f.cam_id == 2);
|
|
CHECK(f.timestamp_ms == 5050);
|
|
}
|
|
|
|
TEST_CASE("start and stop reach every source") {
|
|
Rig r;
|
|
r.src->start();
|
|
CHECK(r.colour->started);
|
|
CHECK(r.thermal->started);
|
|
r.src->stop();
|
|
CHECK_FALSE(r.thermal->started);
|
|
}
|
|
|
|
TEST_CASE("a follower that will not open does not stop the run; the leader does") {
|
|
{
|
|
Rig r;
|
|
r.thermal->throws_on_open = true;
|
|
CHECK_NOTHROW(r.src->open());
|
|
}
|
|
{
|
|
Rig r;
|
|
r.colour->throws_on_open = true;
|
|
CHECK_THROWS(r.src->open());
|
|
}
|
|
}
|