#include "fgc/CompositeCameraSource.h" #include #include #include 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 deviceInfo() override { std::vector 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 src; std::vector frames; explicit Rig(int colour_count = 1) { auto c = std::make_unique(colour_count, 1000); auto t = std::make_unique(1, 5000); colour = c.get(); thermal = t.get(); std::vector> v; v.push_back(std::move(c)); v.push_back(std::move(t)); src = std::make_unique(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()); } }