#include #include "fgc/CameraSettingsXml.h" #include #include #include #include #include using namespace fgc; namespace fs = std::filesystem; namespace { fs::path scratch(const std::string& tag) { fs::path d = fs::temp_directory_path() / ("fgc_xml_" + 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, std::ios::binary); f << text; } // A miniature Viewer export: the two blocks that matter, plus a SelectorGroup. const char* kXml = R"( )"; const SettingEntry* find(const CameraSettings& s, const std::string& name, const std::string& selector) { for (const auto& e : s.entries) if (e.name == name && e.selector == selector) return &e; return nullptr; } } // namespace TEST_CASE("parseCameraSettingsXml reads RemoteDevice only, in order") { fs::path d = scratch("parse"); write(d / "p.xml", kXml); CameraSettings s = parseCameraSettingsXml((d / "p.xml").string()); CHECK(s.model == "1800 U-2040c"); CHECK(s.device_id == "DEV_1AB22C02B9F4"); // LocalDevice describes the host's transport layer and must never be replayed. CHECK(find(s, "DeviceDisplayName", "") == nullptr); REQUIRE(s.entries.size() == 4); CHECK(s.entries[0].name == "AcquisitionFrameRate"); CHECK(s.entries[3].name == "Gamma"); // A repeated feature name stays distinguishable by its selector, or the two // white-balance ratios would collapse into one. const auto* red = find(s, "BalanceRatio", "BalanceRatioSelector=Red"); const auto* blue = find(s, "BalanceRatio", "BalanceRatioSelector=Blue"); REQUIRE(red); REQUIRE(blue); CHECK(red->value == "2.87"); CHECK(blue->value == "2.00"); fs::remove_all(d); } TEST_CASE("parseCameraSettingsXml rejects anything that is not a Viewer export") { fs::path d = scratch("bad"); write(d / "empty.xml", ""); CHECK_THROWS(parseCameraSettingsXml((d / "empty.xml").string())); // no RemoteDevice write(d / "trunc.xml", ""); CHECK_THROWS(parseCameraSettingsXml((d / "trunc.xml").string())); // unterminated write(d / "nofeat.xml", ""); CHECK_THROWS(parseCameraSettingsXml((d / "nofeat.xml").string())); // no features CHECK_THROWS(parseCameraSettingsXml((d / "missing.xml").string())); fs::remove_all(d); } TEST_CASE("the record round-trips: render then read back gives the same settings") { fs::path d = scratch("record"); write(d / "p.xml", kXml); CameraSettings s = parseCameraSettingsXml((d / "p.xml").string()); const std::string text = renderSettingsRecord(s, "p.xml", "fnv1a64:0123456789abcdef"); // The header has to be unmissable: this file configures nothing. CHECK(text.find("DOES NOT CONFIGURE THE CAMERA") != std::string::npos); CHECK(text.find("VimbaX Viewer") != std::string::npos); write(d / "p.txt", text); SettingsRecord rec = readSettingsRecord((d / "p.txt").string()); CHECK(rec.hash_label == "fnv1a64:0123456789abcdef"); REQUIRE(rec.entries.size() == s.entries.size()); for (size_t i = 0; i < s.entries.size(); ++i) { CHECK(rec.entries[i].name == s.entries[i].name); CHECK(rec.entries[i].value == s.entries[i].value); CHECK(rec.entries[i].selector == s.entries[i].selector); } fs::remove_all(d); } TEST_CASE("readSettingsRecord treats a missing file as no record") { SettingsRecord rec = readSettingsRecord("/nonexistent/nope.txt"); CHECK(rec.hash_label.empty()); CHECK(rec.entries.empty()); } TEST_CASE("diffSettings names what changed, not just that something did") { std::vector before = { {"AcquisitionFrameRate", "10", ""}, {"ExposureAutoMax", "20000", ""}, {"Sharpness", "0", ""}, {"BalanceRatio", "2.87", "BalanceRatioSelector=Red"}, }; std::vector after = { {"AcquisitionFrameRate", "8", ""}, // changed {"ExposureAutoMax", "20000", ""}, // same {"Gamma", "1.2", ""}, // new {"BalanceRatio", "3.10", "BalanceRatioSelector=Red"}, // changed, per selector }; // Sharpness removed auto d = diffSettings(before, after); auto has = [&](const std::string& frag) { return std::any_of(d.begin(), d.end(), [&](const std::string& l) { return l.find(frag) != std::string::npos; }); }; CHECK(has("AcquisitionFrameRate 10 -> 8")); CHECK(has("+ Gamma")); CHECK(has("- Sharpness")); CHECK(has("BalanceRatio [BalanceRatioSelector=Red] 2.87 -> 3.10")); // An unchanged setting must not be reported, or the diff becomes noise. CHECK_FALSE(has("ExposureAutoMax")); CHECK(d.size() == 4); CHECK(diffSettings(before, before).empty()); } TEST_CASE("diffSettings handles a name repeated under the SAME selector") { // A real Viewer export nests SelectorGroups, so the same selector name AND // value can appear many times (FileOperationSelector=Open, once per // FileSelector group). Keying only on selector+name collapsed them and // reported every duplicate after the first as new. std::vector v = { {"FileOperationStatus", "Success", "FileOperationSelector=Open"}, {"FileOperationStatus", "Success", "FileOperationSelector=Open"}, {"FileOperationStatus", "Success", "FileOperationSelector=Open"}, }; CHECK(diffSettings(v, v).empty()); // ...and a genuine change to one of them is still reported, exactly once. auto changed = v; changed[1].value = "Failure"; auto d = diffSettings(v, changed); REQUIRE(d.size() == 1); CHECK(d[0].find("Success -> Failure") != std::string::npos); } TEST_CASE("an unchanged real export produces an empty diff") { for (const char* rel : {"../../camera_config_test.xml", "../camera_config_test.xml"}) { if (!fs::is_regular_file(rel)) continue; CameraSettings s = parseCameraSettingsXml(rel); // The strongest possible statement: 800+ real settings, nothing changed, // nothing reported. This is what caught the duplicate-key bug. CHECK(diffSettings(s.entries, s.entries).empty()); return; } MESSAGE("camera_config_test.xml not reachable from the test working directory; skipped"); } TEST_CASE("the real VimbaX export parses") { // Guards against the scanner drifting from what the Viewer actually writes. for (const char* rel : {"../../camera_config_test.xml", "../camera_config_test.xml"}) { if (!fs::is_regular_file(rel)) continue; CameraSettings s = parseCameraSettingsXml(rel); CHECK(s.model == "1800 U-2040c"); CHECK(s.entries.size() > 100); // RemoteDevice only: a LocalDevice-only feature must not have leaked in. CHECK(find(s, "DeviceLocation", "") == nullptr); return; } MESSAGE("camera_config_test.xml not reachable from the test working directory; skipped"); }