ESPSomfy-RTS/test/test_shadetx/test_shadetx.cpp
cjkas 3a2702af6c test: name the ticket tests as GLOSSARY.md does
Two test names still said seq after the rename to ticket.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:35:15 +02:00

245 lines
9.3 KiB
C++

#include <unity.h>
#include "ShadeMotion.h"
#define ASSERT_POS(expected, actual) TEST_ASSERT_FLOAT_WITHIN(0.01f, (expected), (actual))
static const int8_t DOWN = 1;
static const int8_t UP = -1;
static const bool IDLE = false;
static const bool MOVING = true;
static somfy_tx_event_t evt(tx_event_t type, uint32_t ticket, uint32_t at = 0) {
somfy_tx_event_t e;
e.type = type;
e.ticket = ticket;
e.at = at;
return e;
}
// A shade whose idle motor was set moving by radio job ticket.
static ShadeTxState heldBy(uint32_t ticket) {
ShadeTxState tx;
tx.hold.mark(ticket, false, 0);
return tx;
}
void setUp() {}
void tearDown() {}
/*********************************************************************
* The job that set the shade moving
********************************************************************/
static void test_held_job_started_starts_the_movement() {
ShadeTxState tx = heldBy(7);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::started, 7, 1234), MOVING);
TEST_ASSERT_TRUE(act.startMovement);
TEST_ASSERT_EQUAL_UINT32(1234, act.startAt);
TEST_ASSERT_FALSE(act.cancelMovement);
TEST_ASSERT_FALSE(tx.hold.held());
}
static void test_held_job_finished_after_start_does_nothing() {
ShadeTxState tx = heldBy(7);
tx.onEvent(evt(tx_event_t::started, 7, 1234), MOVING);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::finished, 7, 2000), MOVING);
TEST_ASSERT_FALSE(act.any());
}
static void test_held_job_dropped_cancels_the_movement() {
ShadeTxState tx = heldBy(7);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 7), MOVING);
TEST_ASSERT_TRUE(act.cancelMovement);
TEST_ASSERT_FALSE(act.startMovement);
TEST_ASSERT_FALSE(tx.hold.held());
}
static void test_held_job_aborted_before_start_cancels_the_movement() {
ShadeTxState tx = heldBy(7);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::aborted, 7), MOVING);
TEST_ASSERT_TRUE(act.cancelMovement);
}
/*********************************************************************
* The favorite (long press My)
********************************************************************/
static void test_favorite_started_waits_for_the_end() {
ShadeTxState tx;
tx.setMySent(9, 40.0f, 10.0f);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::started, 9), IDLE);
TEST_ASSERT_FALSE(act.any());
TEST_ASSERT_EQUAL_UINT32(9, tx.myTicket);
}
static void test_favorite_finished_is_committed() {
ShadeTxState tx;
tx.setMySent(9, 40.0f, 10.0f);
tx.onEvent(evt(tx_event_t::started, 9), IDLE);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::finished, 9), IDLE);
TEST_ASSERT_TRUE(act.commitMy);
TEST_ASSERT_FALSE(act.restoreMy);
TEST_ASSERT_EQUAL_UINT32(0, tx.myTicket);
}
static void test_favorite_aborted_restores_the_previous_one() {
// The motor only stores the favorite when it gets the whole long press.
ShadeTxState tx;
tx.setMySent(9, 40.0f, 10.0f);
tx.onEvent(evt(tx_event_t::started, 9), IDLE);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::aborted, 9), IDLE);
TEST_ASSERT_TRUE(act.restoreMy);
TEST_ASSERT_FALSE(act.commitMy);
ASSERT_POS(40.0f, act.myOldPos);
ASSERT_POS(10.0f, act.myOldTiltPos);
TEST_ASSERT_EQUAL_UINT32(0, tx.myTicket);
}
static void test_favorite_dropped_restores_the_previous_one() {
ShadeTxState tx;
tx.setMySent(9, -1.0f, -1.0f);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 9), IDLE);
TEST_ASSERT_TRUE(act.restoreMy);
ASSERT_POS(-1.0f, act.myOldPos);
ASSERT_POS(-1.0f, act.myOldTiltPos);
}
/*********************************************************************
* The stop sent at the target
********************************************************************/
static void test_stop_dropped_runs_on_to_the_limit() {
ShadeTxState tx;
tx.stopSent(12, 5000, DOWN, false);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12, 5300), IDLE);
TEST_ASSERT_TRUE(act.runToLimit);
TEST_ASSERT_EQUAL_INT8(DOWN, act.limitDir);
TEST_ASSERT_FALSE(act.limitTilt);
// From when the motor should have stopped, not when the drop was reported.
TEST_ASSERT_EQUAL_UINT32(5000, act.limitSince);
TEST_ASSERT_EQUAL_UINT32(0, tx.stopTicket);
}
static void test_tilt_stop_dropped_runs_the_tilt_on() {
ShadeTxState tx;
tx.stopSent(12, 5000, UP, true);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12), IDLE);
TEST_ASSERT_TRUE(act.runToLimit);
TEST_ASSERT_EQUAL_INT8(UP, act.limitDir);
TEST_ASSERT_TRUE(act.limitTilt);
}
static void test_stop_dropped_after_moving_again_does_nothing() {
// Something set the shade moving since, so that command decides where the
// motor goes.
ShadeTxState tx;
tx.stopSent(12, 5000, DOWN, false);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 12), MOVING);
TEST_ASSERT_FALSE(act.any());
TEST_ASSERT_EQUAL_UINT32(0, tx.stopTicket);
}
static void test_stop_on_air_does_nothing() {
ShadeTxState tx;
tx.stopSent(12, 5000, DOWN, false);
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::started, 12), IDLE).any());
TEST_ASSERT_EQUAL_UINT32(0, tx.stopTicket);
// Nothing is waiting on it any more.
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 12), IDLE).any());
}
static void test_stop_aborted_does_not_run_on() {
// Part of the stop went on air, which is enough for the motor.
ShadeTxState tx;
tx.stopSent(12, 5000, DOWN, false);
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::aborted, 12), IDLE).any());
}
/*********************************************************************
* Edge cases
********************************************************************/
static void test_one_event_for_the_held_job_and_the_favorite() {
// The delayed My that stores the favorite also sets an idle shade moving,
// so the same job is held and awaited as the favorite.
ShadeTxState tx = heldBy(9);
tx.setMySent(9, 40.0f, 10.0f);
ShadeTxActions act = tx.onEvent(evt(tx_event_t::dropped, 9), MOVING);
TEST_ASSERT_TRUE(act.cancelMovement);
TEST_ASSERT_TRUE(act.restoreMy);
TEST_ASSERT_FALSE(tx.hold.held());
TEST_ASSERT_EQUAL_UINT32(0, tx.myTicket);
}
static void test_ticket_zero_is_ignored() {
ShadeTxState tx;
tx.setMySent(0, 40.0f, 10.0f);
tx.stopSent(0, 5000, DOWN, false);
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 0), IDLE).any());
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::finished, 0), IDLE).any());
}
static void test_other_jobs_are_ignored() {
ShadeTxState tx = heldBy(7);
tx.setMySent(9, 40.0f, 10.0f);
tx.stopSent(12, 5000, DOWN, false);
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::dropped, 8), IDLE).any());
TEST_ASSERT_FALSE(tx.onEvent(evt(tx_event_t::finished, 13), IDLE).any());
TEST_ASSERT_EQUAL_UINT32(7, tx.hold.ticket);
TEST_ASSERT_EQUAL_UINT32(9, tx.myTicket);
TEST_ASSERT_EQUAL_UINT32(12, tx.stopTicket);
}
/*********************************************************************
* limitTarget
********************************************************************/
static void test_limitTarget() {
ASSERT_POS(100.0f, limitTarget(DOWN));
ASSERT_POS(0.0f, limitTarget(UP));
}
/*********************************************************************
* TxHold::check
********************************************************************/
static void test_check_nothing_held() {
TxHold hold;
TEST_ASSERT_TRUE(hold.check(true, 1000, 30000) == TxHoldCheck::none);
}
static void test_check_idle_shade_drops_the_hold() {
// The command didn't set the shade moving so there is nothing to hold.
TxHold hold;
hold.mark(5, false, 1000);
TEST_ASSERT_TRUE(hold.check(false, 1000, 30000) == TxHoldCheck::none);
TEST_ASSERT_FALSE(hold.held());
}
static void test_check_holds_a_moving_shade() {
TxHold hold;
hold.mark(5, false, 1000);
TEST_ASSERT_TRUE(hold.check(true, 30999, 30000) == TxHoldCheck::holding);
TEST_ASSERT_TRUE(hold.held());
}
static void test_check_expires() {
TxHold hold;
hold.mark(5, false, 1000);
TEST_ASSERT_TRUE(hold.check(true, 31000, 30000) == TxHoldCheck::expired);
TEST_ASSERT_FALSE(hold.held());
TEST_ASSERT_TRUE(hold.check(true, 31001, 30000) == TxHoldCheck::none);
}
static void test_check_across_millis_wrap() {
TxHold hold;
hold.mark(5, false, 0xFFFFF000u);
TEST_ASSERT_TRUE(hold.check(true, 0x00006000u, 30000) == TxHoldCheck::holding);
TEST_ASSERT_TRUE(hold.check(true, 0x00007000u, 30000) == TxHoldCheck::expired);
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_held_job_started_starts_the_movement);
RUN_TEST(test_held_job_finished_after_start_does_nothing);
RUN_TEST(test_held_job_dropped_cancels_the_movement);
RUN_TEST(test_held_job_aborted_before_start_cancels_the_movement);
RUN_TEST(test_favorite_started_waits_for_the_end);
RUN_TEST(test_favorite_finished_is_committed);
RUN_TEST(test_favorite_aborted_restores_the_previous_one);
RUN_TEST(test_favorite_dropped_restores_the_previous_one);
RUN_TEST(test_stop_dropped_runs_on_to_the_limit);
RUN_TEST(test_tilt_stop_dropped_runs_the_tilt_on);
RUN_TEST(test_stop_dropped_after_moving_again_does_nothing);
RUN_TEST(test_stop_on_air_does_nothing);
RUN_TEST(test_stop_aborted_does_not_run_on);
RUN_TEST(test_one_event_for_the_held_job_and_the_favorite);
RUN_TEST(test_ticket_zero_is_ignored);
RUN_TEST(test_other_jobs_are_ignored);
RUN_TEST(test_limitTarget);
RUN_TEST(test_check_nothing_held);
RUN_TEST(test_check_idle_shade_drops_the_hold);
RUN_TEST(test_check_holds_a_moving_shade);
RUN_TEST(test_check_expires);
RUN_TEST(test_check_across_millis_wrap);
return UNITY_END();
}