fwt_software/tests/test_cameraprofile.cpp

148 lines
4.9 KiB
C++

#include <doctest/doctest.h>
#include "fgc/CameraProfile.h"
#include "fgc/Hash.h"
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <string>
#include <unistd.h>
using namespace fgc;
namespace fs = std::filesystem;
namespace {
fs::path scratch(const std::string& tag) {
fs::path d = fs::temp_directory_path() / ("fgc_prof_" + tag + "_" + std::to_string(::getpid()));
fs::remove_all(d);
fs::create_directories(d);
return d;
}
std::string xmlWith(const std::string& rate) {
return
"<ModuleSettings><CameraInfo Id=\"DEV_1\" Model=\"1800 U-2040c\">"
"<RemoteDevice>"
"<Feature Name=\"AcquisitionFrameRate\" Value=\"" + rate + "\" Type=\"Float\" />"
"<Feature Name=\"Gamma\" Value=\"1\" Type=\"Float\" />"
"</RemoteDevice></CameraInfo></ModuleSettings>";
}
void write(const fs::path& p, const std::string& text) {
std::ofstream f(p, std::ios::binary);
f << text;
}
} // namespace
TEST_CASE("loadCameraProfile generates the record on first load") {
fs::path d = scratch("first");
write(d / "standard.xml", xmlWith("10"));
ProfileLoadResult r;
CameraProfile p = loadCameraProfile(d.string(), "standard", r);
CHECK(p.name == "standard");
CHECK(p.xml_path == (d / "standard.xml").string());
CHECK(p.txt_path == (d / "standard.txt").string());
CHECK(fs::is_regular_file(p.txt_path));
CHECK(p.settings.entries.size() == 2);
// A first load has no baseline: reporting every setting as "new" would be
// thousands of lines and no information for a real export.
CHECK(r.regenerated);
CHECK(r.first_run);
CHECK(r.changes.empty());
// The record carries the hash of the XML it came from.
SettingsRecord rec = readSettingsRecord(p.txt_path);
CHECK(rec.hash_label == hashLabel(fnv1a64File(p.xml_path)));
fs::remove_all(d);
}
TEST_CASE("an unchanged XML is not regenerated and reports no changes") {
fs::path d = scratch("stable");
write(d / "standard.xml", xmlWith("10"));
ProfileLoadResult r;
loadCameraProfile(d.string(), "standard", r);
const auto stamp = fs::last_write_time(d / "standard.txt");
loadCameraProfile(d.string(), "standard", r);
CHECK_FALSE(r.regenerated);
CHECK(r.changes.empty());
// Untouched: a rewrite every startup would churn the file and its timestamp.
CHECK(fs::last_write_time(d / "standard.txt") == stamp);
fs::remove_all(d);
}
TEST_CASE("a changed XML regenerates the record and names what moved") {
fs::path d = scratch("changed");
write(d / "standard.xml", xmlWith("10"));
ProfileLoadResult r;
loadCameraProfile(d.string(), "standard", r);
write(d / "standard.xml", xmlWith("8"));
CameraProfile p = loadCameraProfile(d.string(), "standard", r);
CHECK(r.regenerated);
CHECK_FALSE(r.first_run); // there WAS a baseline, so this is a real change
REQUIRE(r.changes.size() == 1);
CHECK(r.changes[0].find("AcquisitionFrameRate") != std::string::npos);
CHECK(r.changes[0].find("10 -> 8") != std::string::npos);
// And the record now matches the new XML.
SettingsRecord rec = readSettingsRecord(p.txt_path);
CHECK(rec.hash_label == hashLabel(fnv1a64File(p.xml_path)));
fs::remove_all(d);
}
TEST_CASE("a missing XML throws - the record is never a fallback") {
fs::path d = scratch("missing");
// A record on its own must not make the profile loadable: it is a record, not
// a configuration source, and applying one would defeat the GUI validation
// that the XML exists to guarantee.
write(d / "standard.txt", "[Source]\nxml_hash = fnv1a64:0000000000000000\n[Settings]\nGamma = 1\n");
ProfileLoadResult r;
CHECK_THROWS(loadCameraProfile(d.string(), "standard", r));
fs::remove_all(d);
}
TEST_CASE("loadCameraProfiles picks up every *.xml and fails loudly when there are none") {
fs::path d = scratch("dir");
std::map<std::string, ProfileLoadResult> results;
CHECK_THROWS(loadCameraProfiles(d.string(), results)); // empty directory
CHECK_THROWS(loadCameraProfiles((d / "nope").string(), results)); // missing directory
write(d / "standard.xml", xmlWith("10"));
write(d / "night.xml", xmlWith("1"));
write(d / "notes.md", "not a profile");
auto profiles = loadCameraProfiles(d.string(), results);
CHECK(profiles.size() == 2);
CHECK(profiles.count("standard") == 1);
CHECK(profiles.count("night") == 1);
CHECK(profiles.count("notes") == 0);
// Both were written for the first time, and neither reports a change.
REQUIRE(results.size() == 2);
for (const auto& [name, r] : results) {
CHECK(r.first_run);
CHECK(r.changes.empty());
}
// One bad profile fails the whole load, so a profile nobody uses today cannot
// rot unnoticed until the night someone switches to it.
write(d / "broken.xml", "<ModuleSettings></ModuleSettings>");
CHECK_THROWS(loadCameraProfiles(d.string(), results));
fs::remove_all(d);
}