125 lines
4.7 KiB
C++
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));
|
|
}
|