fwt_software/tests/test_cameramode.cpp

169 lines
5.5 KiB
C++

#include <doctest/doctest.h>
#include "fgc/CameraMode.h"
#include <filesystem>
#include <fstream>
#include <string>
#include <unistd.h>
using namespace fgc;
namespace fs = std::filesystem;
namespace {
// Each test gets its own directory so a stale file can never leak between them.
fs::path scratchDir(const std::string& tag) {
fs::path d = fs::temp_directory_path() / ("fgc_modes_" + tag + "_" + std::to_string(::getpid()));
fs::remove_all(d);
fs::create_directories(d);
return d;
}
void write(const fs::path& p, const std::string& text) {
std::ofstream f(p);
f << text;
}
const char* kMinimal = R"(
[Mode]
description = A mode
settle_wait_ms = 1500
frame_timeout_ms = 3000
[Camera]
AcquisitionFrameRate = 10
ExposureAuto = Continuous
)";
} // namespace
TEST_CASE("loadCameraMode reads the header and names the mode after the file") {
fs::path d = scratchDir("header");
write(d / "standard.ini", kMinimal);
CameraMode m = loadCameraMode((d / "standard.ini").string());
CHECK(m.name == "standard");
CHECK(m.description == "A mode");
CHECK(m.settle_wait_ms == 1500);
CHECK(m.frame_timeout_ms == 3000);
REQUIRE(m.features.size() == 2);
fs::remove_all(d);
}
TEST_CASE("loadCameraMode defaults the [Mode] header when it is absent") {
fs::path d = scratchDir("defaults");
write(d / "bare.ini", "[Camera]\nGamma = 1.0\n");
CameraMode m = loadCameraMode((d / "bare.ini").string());
CHECK(m.description.empty());
CHECK(m.settle_wait_ms == 1000);
CHECK(m.frame_timeout_ms == 2000);
fs::remove_all(d);
}
TEST_CASE("loadCameraMode preserves file order and repeated keys") {
// GenICam selectors are stateful, so both properties are load-bearing: a map
// would sort BalanceRatio ahead of BalanceRatioSelector and collapse the two
// ratios into one.
fs::path d = scratchDir("order");
write(d / "wb.ini", R"(
[Camera]
BalanceRatioSelector = Red
BalanceRatio = 2.87
BalanceRatioSelector = Blue
BalanceRatio = 2.0
)");
CameraMode m = loadCameraMode((d / "wb.ini").string());
REQUIRE(m.features.size() == 4);
CHECK(m.features[0] == std::make_pair(std::string("BalanceRatioSelector"), std::string("Red")));
CHECK(m.features[1] == std::make_pair(std::string("BalanceRatio"), std::string("2.87")));
CHECK(m.features[2] == std::make_pair(std::string("BalanceRatioSelector"), std::string("Blue")));
CHECK(m.features[3] == std::make_pair(std::string("BalanceRatio"), std::string("2.0")));
fs::remove_all(d);
}
TEST_CASE("loadCameraMode strips inline comments from feature values") {
fs::path d = scratchDir("comments");
write(d / "c.ini", "[Camera]\nExposureAutoMax = 20000 ; blur budget\n");
CameraMode m = loadCameraMode((d / "c.ini").string());
REQUIRE(m.features.size() == 1);
CHECK(m.features[0].second == "20000");
fs::remove_all(d);
}
TEST_CASE("loadCameraMode rejects files a typo would otherwise make silently wrong") {
fs::path d = scratchDir("bad");
write(d / "unknown_key.ini", "[Mode]\nsettle_wait = 500\n[Camera]\nGamma = 1\n");
CHECK_THROWS(loadCameraMode((d / "unknown_key.ini").string()));
write(d / "unknown_section.ini", "[Modes]\ndescription = x\n[Camera]\nGamma = 1\n");
CHECK_THROWS(loadCameraMode((d / "unknown_section.ini").string()));
write(d / "bad_value.ini", "[Mode]\nsettle_wait_ms = soon\n[Camera]\nGamma = 1\n");
CHECK_THROWS(loadCameraMode((d / "bad_value.ini").string()));
write(d / "negative.ini", "[Mode]\nsettle_wait_ms = -1\n[Camera]\nGamma = 1\n");
CHECK_THROWS(loadCameraMode((d / "negative.ini").string()));
// A mode that sets nothing on the camera is a mistake, not an empty mode.
write(d / "no_features.ini", "[Mode]\ndescription = nothing\n");
CHECK_THROWS(loadCameraMode((d / "no_features.ini").string()));
CHECK_THROWS(loadCameraMode((d / "does_not_exist.ini").string()));
fs::remove_all(d);
}
TEST_CASE("loadCameraModes picks up every *.ini in the directory") {
fs::path d = scratchDir("dir");
write(d / "standard.ini", kMinimal);
write(d / "night.ini", kMinimal);
write(d / "notes.txt", "not a mode");
auto modes = loadCameraModes(d.string());
CHECK(modes.size() == 2);
CHECK(modes.count("standard") == 1);
CHECK(modes.count("night") == 1);
CHECK(modes.count("notes") == 0);
fs::remove_all(d);
}
TEST_CASE("loadCameraModes fails loudly on an empty or missing directory") {
fs::path d = scratchDir("empty");
CHECK_THROWS(loadCameraModes(d.string()));
CHECK_THROWS(loadCameraModes((d / "nope").string()));
// One bad file fails the whole load, so a mode nobody uses today still cannot
// rot unnoticed until the night someone switches to it.
write(d / "good.ini", kMinimal);
write(d / "bad.ini", "[Mode]\nnonsense = 1\n");
CHECK_THROWS(loadCameraModes(d.string()));
fs::remove_all(d);
}
TEST_CASE("the shipped RGB mode files parse and define both modes") {
// Guards the real config/camera_modes/rgb files, which the rig depends on.
for (const char* rel : {"config/camera_modes/rgb", "../config/camera_modes/rgb"}) {
if (!fs::is_directory(rel)) continue;
auto modes = loadCameraModes(rel);
REQUIRE(modes.count("standard") == 1);
REQUIRE(modes.count("night") == 1);
CHECK(modes.at("standard").settle_wait_ms == 1000);
CHECK(modes.at("night").settle_wait_ms == 2500);
CHECK(modes.at("standard").features.size() > 40);
return;
}
MESSAGE("shipped mode files not reachable from the test working directory; skipped");
}