148 lines
4.9 KiB
C++
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);
|
|
}
|