208 lines
8.0 KiB
C++
208 lines
8.0 KiB
C++
#include <doctest/doctest.h>
|
|
|
|
#include "fgc/CameraSettingsXml.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_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"(<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
|
<ModuleSettings>
|
|
<CameraInfo Id="DEV_1AB22C02B9F4" Model="1800 U-2040c">
|
|
<LocalDevice>
|
|
<Feature Name="DeviceDisplayName" Value="Should Not Appear" Type="String" />
|
|
</LocalDevice>
|
|
<RemoteDevice>
|
|
<Feature Name="AcquisitionFrameRate" Value="10" Type="Float" />
|
|
<SelectorGroup Name="BalanceRatioSelector" Type="Enum" Value="Red">
|
|
<Feature Name="BalanceRatio" Value="2.87" Type="Float" />
|
|
</SelectorGroup>
|
|
<SelectorGroup Name="BalanceRatioSelector" Type="Enum" Value="Blue">
|
|
<Feature Name="BalanceRatio" Value="2.00" Type="Float" />
|
|
</SelectorGroup>
|
|
<Feature Name="Gamma" Value="1" Type="Float" />
|
|
</RemoteDevice>
|
|
</CameraInfo>
|
|
</ModuleSettings>
|
|
)";
|
|
|
|
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", "<ModuleSettings></ModuleSettings>");
|
|
CHECK_THROWS(parseCameraSettingsXml((d / "empty.xml").string())); // no RemoteDevice
|
|
|
|
write(d / "trunc.xml", "<ModuleSettings><RemoteDevice><Feature Name=\"A\" Value=\"1\" />");
|
|
CHECK_THROWS(parseCameraSettingsXml((d / "trunc.xml").string())); // unterminated
|
|
|
|
write(d / "nofeat.xml", "<RemoteDevice></RemoteDevice>");
|
|
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<SettingEntry> before = {
|
|
{"AcquisitionFrameRate", "10", ""},
|
|
{"ExposureAutoMax", "20000", ""},
|
|
{"Sharpness", "0", ""},
|
|
{"BalanceRatio", "2.87", "BalanceRatioSelector=Red"},
|
|
};
|
|
std::vector<SettingEntry> 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<SettingEntry> 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");
|
|
}
|