#include #include "fgc/ExposureStore.h" #include #include #include 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)); }