fwt_software/tests/test_camerasettingsxml.cpp

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");
}