fwt_software/tests/test_composite.cpp

240 lines
7.8 KiB
C++

#include "fgc/CompositeCameraSource.h"
#include <doctest/doctest.h>
#include <chrono>
#include <stdexcept>
#include <thread>
#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 counts_after_open = false; // as Vimba: no cameras until open()
bool opened = false;
bool ready = true;
bool throws_on_open = false;
int triggers = 0;
bool started = false;
bool frame_ready = true; // frameReady()
int moves = 0;
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");
opened = true;
}
void close() override {}
void start() override { started = true; }
void stop() override { started = false; }
void moving() override { ++moves; }
bool frameReady() override { return frame_ready; }
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 counts_after_open && !opened ? 0 : 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("the indices are the configured ones, also before the leader has opened") {
// On tower 111 the thermal frame came out as camera 0 - "RGB", overwriting
// the colour file of the same time - because Vimba counts no camera before
// open() and the offsets were taken from that.
auto c = std::make_unique<FakeSource>(1, 1000);
auto t = std::make_unique<FakeSource>(1, 5000);
c->counts_after_open = true;
FakeSource* colour = c.get();
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::move(c));
v.push_back(std::move(t));
CompositeCameraSource src(std::move(v), {1, 1});
std::vector<Frame> frames;
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
CHECK(src.cameraCount() == 1); // what is open: the thermal camera only
src.open();
CHECK(colour->opened);
CHECK(src.cameraCount() == 2);
REQUIRE(src.trigger());
REQUIRE(frames.size() == 2);
CHECK(frames[0].cam_id == 0);
CHECK(frames[1].cam_id == 1);
CHECK(src.offline(1).empty());
CHECK_THROWS(CompositeCameraSource(std::vector<std::unique_ptr<ICameraSource>>{}, {}));
}
TEST_CASE("a picture waits a moment for a follower between frames, then goes without it") {
auto c = std::make_unique<FakeSource>(1, 1000);
auto t = std::make_unique<FakeSource>(1, 5000);
FakeSource* colour = c.get();
FakeSource* thermal = t.get();
std::vector<std::unique_ptr<ICameraSource>> v;
v.push_back(std::move(c));
v.push_back(std::move(t));
CompositeCameraSource src(std::move(v), {1, 1}, 60);
std::vector<Frame> frames;
src.setFrameCallback([&](const Frame& f) { frames.push_back(f); });
// The thermal camera is in its NUC: the picture is held, the leader not asked.
thermal->frame_ready = false;
CHECK_FALSE(src.trigger());
CHECK(colour->triggers == 0);
CHECK_FALSE(src.frameReady());
// It comes back within the wait: one whole pair.
thermal->frame_ready = true;
REQUIRE(src.trigger());
CHECK(frames.size() == 2);
// It does not come back: after the wait the colour picture goes alone.
frames.clear();
thermal->frame_ready = false;
CHECK_FALSE(src.trigger());
std::this_thread::sleep_for(std::chrono::milliseconds(80));
REQUIRE(src.trigger());
CHECK(colour->triggers == 2);
}
TEST_CASE("every source hears that the gimbal moves") {
Rig r;
r.src->moving();
CHECK(r.colour->moves == 1);
CHECK(r.thermal->moves == 1);
}
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());
}
}