mirror of
https://github.com/bod09/frame-autopass.git
synced 2026-10-06 00:00:05 +02:00
274 lines
11 KiB
C++
274 lines
11 KiB
C++
// Host-side tests for LightPolicy: evidence in, switching decisions out.
|
|
// Sensor behaviour is tested in test_sensors.cpp.
|
|
|
|
#include "light_policy.hpp"
|
|
|
|
#include <cstdio>
|
|
#include <functional>
|
|
#include <vector>
|
|
|
|
using namespace autopass;
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void check(bool ok, const char* what, int line) {
|
|
if (!ok) {
|
|
std::printf("FAIL line %d: %s\n", line, what);
|
|
++failures;
|
|
}
|
|
}
|
|
#define CHECK(expr) check((expr), #expr, __LINE__)
|
|
|
|
// Feeds evidence(t) every `step_ms` from `from_ms` to `to_ms`; returns the
|
|
// switch times.
|
|
std::vector<std::uint64_t> run(LightPolicy& p, std::uint64_t from_ms, std::uint64_t to_ms,
|
|
const std::function<Evidence(std::uint64_t)>& evidence, std::uint64_t step_ms = 250) {
|
|
std::vector<std::uint64_t> switches;
|
|
for (std::uint64_t t = from_ms; t <= to_ms; t += step_ms)
|
|
if (p.update(t, evidence(t)).changed) switches.push_back(t);
|
|
return switches;
|
|
}
|
|
|
|
// Evidence for the source currently shown in a room that is dark when
|
|
// `dark(t)`: Dark while colour is shown in the dark, Bright while IR is
|
|
// shown in the light, otherwise Neutral.
|
|
std::function<Evidence(std::uint64_t)> room(LightPolicy& p, const std::function<bool(std::uint64_t)>& dark) {
|
|
return [&p, dark](std::uint64_t t) {
|
|
const bool d = dark(t);
|
|
if (p.source() == Source::Color) return d ? Evidence::Dark : Evidence::Neutral;
|
|
return d ? Evidence::Neutral : Evidence::Bright;
|
|
};
|
|
}
|
|
|
|
// The timing rules are tested with a 0.5 s confirmation; the defaults are
|
|
// covered by test_sensors' end-to-end replays.
|
|
PolicyConfig cfg() {
|
|
PolicyConfig c;
|
|
c.confirm_s = 0.5;
|
|
return c;
|
|
}
|
|
|
|
void test_validate_and_strings() {
|
|
CHECK(validate(PolicyConfig{}).empty());
|
|
PolicyConfig c;
|
|
c.confirm_s = -1;
|
|
CHECK(!validate(c).empty());
|
|
c = {};
|
|
c.stale_s = 0;
|
|
CHECK(!validate(c).empty());
|
|
c = {};
|
|
c.cooldown_max_s = 1; // below the 2 s base
|
|
CHECK(!validate(c).empty());
|
|
Mode m;
|
|
CHECK(parse_mode("auto", &m) && m == Mode::Auto);
|
|
CHECK(parse_mode("colour", &m) && m == Mode::ForceColor);
|
|
CHECK(parse_mode("color", &m) && m == Mode::ForceColor);
|
|
CHECK(parse_mode("ir", &m) && m == Mode::ForceIr);
|
|
CHECK(!parse_mode("bright", &m));
|
|
}
|
|
|
|
void test_first_switch_is_fast() {
|
|
// Lights off in a room: the switch comes after confirm_s (0.5 s), not
|
|
// after any cooldown.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 20000, room(p, [](std::uint64_t t) { return t >= 5000; }));
|
|
CHECK(s.size() == 1);
|
|
if (!s.empty()) CHECK(s[0] >= 5500 && s[0] <= 5750);
|
|
}
|
|
|
|
void test_normal_off_then_on_is_fast_both_ways() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 60000, room(p, [](std::uint64_t t) { return t >= 5000 && t < 30000; }));
|
|
CHECK(s.size() == 2);
|
|
if (s.size() == 2) {
|
|
CHECK(s[0] <= 5750);
|
|
CHECK(s[1] >= 30000 && s[1] <= 32250); // settle 1.5 s already long past; confirm 0.5 s
|
|
}
|
|
}
|
|
|
|
void test_quick_reversal_waits_for_base_cooldown() {
|
|
// Light off, then straight back on after 1 s: the reversal waits for
|
|
// the 2 s cooldown, not longer.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 20000, room(p, [](std::uint64_t t) { return t >= 5000 && t < 6000; }));
|
|
CHECK(s.size() == 2);
|
|
if (s.size() == 2) CHECK(s[1] - s[0] >= 2000 && s[1] - s[0] <= 2750);
|
|
}
|
|
|
|
void test_flickering_light_escalates_cooldown() {
|
|
// A light flickering on/off every second for two minutes. Without the
|
|
// cooldown this would switch ~120 times; with it, the gaps double.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 120000, room(p, [](std::uint64_t t) { return (t / 1000) % 2 == 1; }));
|
|
CHECK(s.size() >= 3 && s.size() <= 10);
|
|
for (std::size_t i = 2; i < s.size(); ++i) CHECK(s[i] - s[i - 1] >= s[i - 1] - s[i - 2] - 1000);
|
|
// Gaps reach the 30 s cap and never exceed cap + one flicker period.
|
|
if (s.size() >= 2) CHECK(s.back() - s[s.size() - 2] <= 32000);
|
|
std::printf("flicker: %zu switches in 120 s:", s.size());
|
|
for (auto t : s) std::printf(" %.1f", t / 1000.0);
|
|
std::printf("\n");
|
|
}
|
|
|
|
void test_lights_toggled_by_hand_stay_fast() {
|
|
// Someone switching the lights every 5 s for a minute (2026-10-01 18:55:
|
|
// the old rule made each switch slower than the last). Every change is
|
|
// followed within confirm time; the cooldown never grows.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 62000, room(p, [](std::uint64_t t) { return t >= 2000 && (t - 2000) / 5000 % 2 == 0; }));
|
|
CHECK(s.size() == 12);
|
|
for (std::size_t i = 0; i < s.size(); ++i) {
|
|
const std::uint64_t change = 2000 + 5000 * i;
|
|
CHECK(s[i] >= change && s[i] <= change + 1250);
|
|
}
|
|
CHECK(p.cooldown_ms() == 2000);
|
|
}
|
|
|
|
void test_cooldown_resets_after_quiet_period() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
// Four quick reversals escalate the cooldown...
|
|
run(p, 0, 30000, room(p, [](std::uint64_t t) { return (t / 1000) % 2 == 1; }));
|
|
CHECK(p.cooldown_ms() > 2000);
|
|
// ...then a quiet minute in steady light, and a normal off/on is fast
|
|
// again.
|
|
const bool steady_dark = p.source() == Source::Ir; // keep the light matching what is shown
|
|
run(p, 30250, 100000, room(p, [steady_dark](std::uint64_t) { return steady_dark; }));
|
|
const bool start_dark = p.source() == Source::Ir;
|
|
const auto s = run(p, 100250, 110000, room(p, [start_dark](std::uint64_t) { return !start_dark; }));
|
|
CHECK(s.size() == 1);
|
|
if (!s.empty()) CHECK(s[0] <= 101000);
|
|
}
|
|
|
|
void test_brief_evidence_ignored() {
|
|
// Dark evidence for less than confirm_s (a hand passing) never switches.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
CHECK(run(p, 0, 30000, room(p, [](std::uint64_t t) { return t >= 5000 && t < 5250; })).empty());
|
|
// Evidence flickering faster than confirm_s never switches either.
|
|
LightPolicy q(cfg(), Source::Color);
|
|
int changes = 0;
|
|
for (std::uint64_t t = 0; t < 60000; t += 250)
|
|
if (q.update(t, (t / 250) % 2 ? Evidence::Dark : Evidence::Neutral).changed) ++changes;
|
|
CHECK(changes == 0);
|
|
}
|
|
|
|
void test_unknown_between_samples_keeps_confirming() {
|
|
// A sensor sampling slower than the loop produces Unknown in between;
|
|
// confirmation keeps accumulating across those ticks.
|
|
LightPolicy p(cfg(), Source::Color);
|
|
const auto s = run(p, 0, 5000, [](std::uint64_t t) { return (t / 250) % 2 ? Evidence::Unknown : Evidence::Dark; });
|
|
CHECK(s.size() == 1);
|
|
}
|
|
|
|
void test_stale_readings_hold_and_reset() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
run(p, 0, 2000, [](auto) { return Evidence::Neutral; });
|
|
// Dark for 0.25 s (not yet confirmed), then readings stop for 5 s.
|
|
CHECK(run(p, 2250, 2500, [](auto) { return Evidence::Dark; }).empty());
|
|
CHECK(run(p, 2750, 7750, [](auto) { return Evidence::Unknown; }).empty());
|
|
CHECK(p.source() == Source::Color);
|
|
// Dark again: confirmation restarts from zero.
|
|
const auto s = run(p, 8000, 12000, [](auto) { return Evidence::Dark; });
|
|
CHECK(s.size() == 1);
|
|
if (!s.empty()) CHECK(s[0] >= 8500);
|
|
}
|
|
|
|
void test_manual_override() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
auto d = p.set_mode(1000, Mode::ForceIr);
|
|
CHECK(d.changed && d.source == Source::Ir);
|
|
CHECK(run(p, 1250, 60000, [](auto) { return Evidence::Bright; }).empty());
|
|
d = p.set_mode(60000, Mode::Auto);
|
|
CHECK(!d.changed && d.source == Source::Ir);
|
|
d = p.set_mode(61000, Mode::ForceColor);
|
|
CHECK(d.changed && d.source == Source::Color);
|
|
CHECK(!p.set_mode(62000, Mode::ForceColor).changed);
|
|
}
|
|
|
|
void test_manual_switch_does_not_delay_auto() {
|
|
// Seen live 23:27: force IR in bright light, back to auto; colour
|
|
// returns after settle + confirm, with no cooldown. A long cooldown
|
|
// makes the difference visible past the settle window.
|
|
PolicyConfig c = cfg();
|
|
c.cooldown_base_s = 10;
|
|
LightPolicy p(c, Source::Color);
|
|
CHECK(p.set_mode(5000, Mode::ForceIr).changed);
|
|
p.set_mode(6000, Mode::Auto);
|
|
const auto s = run(p, 6250, 20000, [](auto) { return Evidence::Bright; });
|
|
CHECK(s.size() == 1);
|
|
if (!s.empty()) CHECK(s[0] <= 7250);
|
|
}
|
|
|
|
void test_settle_ignores_transition_evidence() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
CHECK(p.set_mode(1000, Mode::ForceIr).changed);
|
|
p.set_mode(1100, Mode::Auto);
|
|
for (std::uint64_t t = 1250; t < 2500; t += 250)
|
|
CHECK(p.update(t, Evidence::Bright).reason == "settling after switch");
|
|
CHECK(p.update(2600, Evidence::Bright).reason == "confirming");
|
|
}
|
|
|
|
void test_adopt_is_not_a_switch() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
p.adopt(Source::Ir);
|
|
CHECK(p.source() == Source::Ir && !p.settling(0));
|
|
const auto s = run(p, 0, 5000, [](auto) { return Evidence::Bright; });
|
|
CHECK(s.size() == 1);
|
|
if (!s.empty()) CHECK(s[0] <= 750);
|
|
}
|
|
|
|
void test_urgent_skips_cooldown_with_short_confirm() {
|
|
// A switch to colour turned out wrong (it is dark): undoing it must not
|
|
// wait for the cooldown, and needs only urgent_confirm_s (0.25 s).
|
|
PolicyConfig c = cfg();
|
|
c.cooldown_base_s = 10;
|
|
LightPolicy p(c, Source::Ir);
|
|
std::uint64_t to_colour = 0, back = 0;
|
|
for (std::uint64_t t = 0; t < 5000 && !to_colour; t += 250)
|
|
if (p.update(t, Evidence::Bright).changed) to_colour = t;
|
|
CHECK(to_colour > 0);
|
|
for (std::uint64_t t = to_colour + 50; t < 20000 && !back; t += 50)
|
|
if (p.update(t, Evidence::Dark, "", true).changed) back = t;
|
|
CHECK(back && back - to_colour <= 650); // settle 0.3 s + urgent confirm 0.25 s
|
|
// Non-urgent evidence afterwards respects the escalated cooldown (the
|
|
// quick reversal doubled it to 20 s).
|
|
CHECK(p.cooldown_ms() == 20000);
|
|
const auto s2 = run(p, back + 250, back + 40000, [](auto) { return Evidence::Bright; });
|
|
CHECK(s2.size() == 1);
|
|
if (!s2.empty()) CHECK(s2[0] - back >= 20000);
|
|
}
|
|
|
|
void test_time_going_backwards_is_safe() {
|
|
LightPolicy p(cfg(), Source::Color);
|
|
run(p, 0, 5000, [](auto) { return Evidence::Neutral; });
|
|
CHECK(!p.update(4000, Evidence::Neutral).changed);
|
|
CHECK(!p.update(100, Evidence::Dark).changed);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
test_validate_and_strings();
|
|
test_first_switch_is_fast();
|
|
test_normal_off_then_on_is_fast_both_ways();
|
|
test_quick_reversal_waits_for_base_cooldown();
|
|
test_flickering_light_escalates_cooldown();
|
|
test_lights_toggled_by_hand_stay_fast();
|
|
test_cooldown_resets_after_quiet_period();
|
|
test_brief_evidence_ignored();
|
|
test_unknown_between_samples_keeps_confirming();
|
|
test_stale_readings_hold_and_reset();
|
|
test_manual_override();
|
|
test_manual_switch_does_not_delay_auto();
|
|
test_settle_ignores_transition_evidence();
|
|
test_adopt_is_not_a_switch();
|
|
test_urgent_skips_cooldown_with_short_confirm();
|
|
test_time_going_backwards_is_safe();
|
|
if (failures) {
|
|
std::printf("%d check(s) failed\n", failures);
|
|
return 1;
|
|
}
|
|
std::printf("all light policy tests passed\n");
|
|
return 0;
|
|
}
|