fwt_software/tests/test_exposurestore.cpp

125 lines
4.7 KiB
C++

#include <doctest/doctest.h>
#include "fgc/ExposureStore.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
using namespace fgc;
namespace fs = std::filesystem;
namespace {
std::string tempPath(const char* name) {
return (fs::temp_directory_path() / name).string();
}
ImageMetrics metrics(double sharpness, double mean = 110.0) {
ImageMetrics m;
m.valid = true;
m.sharpness = sharpness;
m.mean_luma = mean;
return m;
}
constexpr long long kNow = 1'700'000'000'000LL;
} // namespace
TEST_CASE("keyFor buckets angles so nearby headings share a seed") {
CHECK(ExposureStore::keyFor(137.2, -5.1, 1.0) == ExposureStore::keyFor(137.4, -4.9, 1.0));
CHECK(ExposureStore::keyFor(137.0, 0.0, 1.0) != ExposureStore::keyFor(138.0, 0.0, 1.0));
// A coarser quantum merges more headings into one bucket.
CHECK(ExposureStore::keyFor(10.0, 0.0, 5.0) == ExposureStore::keyFor(12.0, 0.0, 5.0));
// Negative pitch must round consistently, not toward zero.
CHECK(ExposureStore::keyFor(0.0, -7.6, 1.0) == ExposureStore::keyFor(0.0, -7.5, 1.0));
}
TEST_CASE("store round-trips through CSV") {
const std::string path = tempPath("fgc_test_exposure_roundtrip.csv");
fs::remove(path);
{
ExposureStore s(path);
s.update(90.0, 0.0, {4200.0, 3.5}, metrics(1234.0, 108.0), kNow, 2);
s.update(180.0, -5.0, {6000.0, 0.0}, metrics(900.0), kNow, 1);
REQUIRE(s.save());
}
ExposureStore loaded(path);
REQUIRE(loaded.load());
CHECK(loaded.size() == 2);
auto e = loaded.find(90.0, 0.0);
REQUIRE(e.has_value());
CHECK(e->exposure_us == doctest::Approx(4200.0));
CHECK(e->gain_db == doctest::Approx(3.5));
CHECK(e->sharpness == doctest::Approx(1234.0));
CHECK(e->attempts == 2);
CHECK(e->timestamp_ms == kNow);
fs::remove(path);
}
TEST_CASE("a missing store is an empty store, not an error path") {
ExposureStore s(tempPath("fgc_test_exposure_absent.csv"));
fs::remove(s.path());
CHECK_FALSE(s.load()); // reports "nothing loaded"...
CHECK(s.size() == 0); // ...but is perfectly usable
CHECK_FALSE(s.find(0.0, 0.0).has_value());
}
TEST_CASE("a malformed row is skipped rather than failing the whole load") {
// A corrupt line must never stop a scan from running.
const std::string path = tempPath("fgc_test_exposure_corrupt.csv");
{
std::ofstream out(path, std::ios::trunc);
out << "key,yaw_deg,pitch_deg,exposure_us,gain_db,sharpness,mean_luma,timestamp_ms,attempts\n";
out << "y90_p0,90,0,4200,3.5,1234,108," << kNow << ",2\n";
out << "garbage,not,a,number,at,all,here,either,x\n";
out << "too,few,fields\n";
out << "y180_p0,180,0,6000,0,900,110," << kNow << ",1\n";
}
ExposureStore s(path);
REQUIRE(s.load());
CHECK(s.size() == 2);
CHECK(s.find(90.0, 0.0).has_value());
CHECK(s.find(180.0, 0.0).has_value());
fs::remove(path);
}
TEST_CASE("seeding falls through three tiers as entries go stale") {
const CaptureSettings defaults{9999.0, 9.0};
ExposureStore s(tempPath("fgc_test_exposure_seed.csv"), 1.0, /*stale_s=*/1800);
// Tier 3: nothing known at all.
CHECK(s.seed(90.0, 0.0, kNow, defaults).exposure_us == doctest::Approx(9999.0));
s.update(90.0, 0.0, {4200.0, 1.0}, metrics(1000.0), kNow, 1);
// Tier 1: this angle's own fresh entry is the best predictor.
CHECK(s.seed(90.0, 0.0, kNow + 1000, defaults).exposure_us == doctest::Approx(4200.0));
// Tier 2: the angle's entry has gone stale, but something was accepted recently
// elsewhere - light changes affect all angles together, so that beats both the
// stale entry and the cold defaults.
s.update(200.0, 0.0, {5500.0, 2.0}, metrics(800.0), kNow + 3600'000, 1);
auto seeded = s.seed(90.0, 0.0, kNow + 3600'000, defaults);
CHECK(seeded.exposure_us == doctest::Approx(5500.0));
CHECK(seeded.gain_db == doctest::Approx(2.0));
// Everything stale: fall all the way back to the configured defaults.
CHECK(s.seed(90.0, 0.0, kNow + 100'000'000, defaults).exposure_us == doctest::Approx(9999.0));
}
TEST_CASE("reference sharpness is only offered while it is still current") {
ExposureStore s(tempPath("fgc_test_exposure_ref.csv"), 1.0, /*stale_s=*/1800);
CHECK(s.referenceSharpness(90.0, 0.0, kNow) == doctest::Approx(0.0));
s.update(90.0, 0.0, {4200.0, 0.0}, metrics(1500.0), kNow, 1);
CHECK(s.referenceSharpness(90.0, 0.0, kNow + 1000) == doctest::Approx(1500.0));
// Too old to compare against: the scene may have changed character entirely
// (fog rolling in), so returning 0 disables the relative blur check.
CHECK(s.referenceSharpness(90.0, 0.0, kNow + 100'000'000) == doctest::Approx(0.0));
}