mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
The read routes leave out a room, shade, group or linked shade that doesn't fit the 3 KB unit, without telling the client (KNOWN_ISSUES.md). test_snapshot_limits fills a snapshot with the largest possible items and checks every read document written with the real unit is identical to the same document written with an unlimited one. The largest item, a shade, is 1,097 bytes. The unit is now SNAPSHOT_UNIT_SIZE in ModelSnapshot.h, used by Web.cpp and the tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
354 lines
14 KiB
C++
354 lines
14 KiB
C++
#include <unity.h>
|
|
#include <string.h>
|
|
#include <string>
|
|
#include "ModelSnapshot.h"
|
|
|
|
static model_snapshot_t snap;
|
|
static char unitBuf[SNAPSHOT_UNIT_SIZE];
|
|
|
|
static room_snapshot_t room(uint8_t id, const char *name) {
|
|
room_snapshot_t r{};
|
|
r.roomId = id;
|
|
strcpy(r.name, name);
|
|
return r;
|
|
}
|
|
static shade_snapshot_t shade(uint8_t id, const char *name) {
|
|
shade_snapshot_t s{};
|
|
s.shadeId = id;
|
|
s.roomId = 2;
|
|
strcpy(s.name, name);
|
|
s.remoteAddress = 1000 + id;
|
|
s.upTime = 10000;
|
|
s.downTime = 11000;
|
|
s.tiltTime = 7000;
|
|
s.stepSize = 100;
|
|
s.paired = true;
|
|
s.lastRollingCode = 42;
|
|
s.position = 30;
|
|
s.target = 30;
|
|
s.myPos = -1;
|
|
s.tiltType = 2;
|
|
s.tiltPosition = 40;
|
|
s.tiltTarget = 40;
|
|
s.myTiltPos = -1;
|
|
s.shadeType = 1;
|
|
s.bitLength = 56;
|
|
s.inGroup = true;
|
|
s.repeats = 1;
|
|
return s;
|
|
}
|
|
static group_snapshot_t group(uint8_t id, const char *name) {
|
|
group_snapshot_t g{};
|
|
g.groupId = id;
|
|
strcpy(g.name, name);
|
|
g.remoteAddress = 2000 + id;
|
|
g.lastRollingCode = 5;
|
|
g.bitLength = 80;
|
|
g.repeats = 2;
|
|
return g;
|
|
}
|
|
static std::string json(void (*write)(JsonFormatter &)) {
|
|
static char buf[2048];
|
|
BufferedJsonFormatter fmt;
|
|
fmt.setBuffer(buf, sizeof(buf));
|
|
fmt.beginObject();
|
|
write(fmt);
|
|
fmt.endObject();
|
|
return std::string(buf, fmt.length());
|
|
}
|
|
static std::string shadeRefJson(uint8_t id) {
|
|
static uint8_t which;
|
|
which = id;
|
|
return json([](JsonFormatter &f) { writeShadeRef(f, *snap.shade(which)); });
|
|
}
|
|
static std::string shadeJson(uint8_t id) {
|
|
static uint8_t which;
|
|
which = id;
|
|
return json([](JsonFormatter &f) { writeShade(f, *snap.shade(which)); });
|
|
}
|
|
static std::string roomJson(uint8_t id) {
|
|
static uint8_t which;
|
|
which = id;
|
|
return json([](JsonFormatter &f) { writeRoom(f, *snap.room(which)); });
|
|
}
|
|
static std::string groupRefJson(uint8_t id) {
|
|
static uint8_t which;
|
|
which = id;
|
|
return json([](JsonFormatter &f) { writeGroupRef(f, *snap.group(which)); });
|
|
}
|
|
static void testHeader(JsonFormatter &json, const model_snapshot_t &s) {
|
|
json.addElem("startingAddress", s.startingAddress);
|
|
}
|
|
// The whole document, taken chunkSize bytes at a time like the web server does.
|
|
static std::string document(snapshot_doc_t doc, uint8_t id = 0, size_t chunkSize = 7, size_t unitSize = sizeof(unitBuf)) {
|
|
SnapshotWriter writer(snap, doc, id, testHeader, unitBuf, unitSize);
|
|
std::string out;
|
|
uint8_t chunk[64];
|
|
for(int guard = 0; guard < 100000; guard++) {
|
|
const size_t n = writer.generate(chunk, chunkSize);
|
|
if(n == 0) break;
|
|
out.append(reinterpret_cast<char *>(chunk), n);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void setUp() {
|
|
snap = model_snapshot_t{};
|
|
snap.startingAddress = 77;
|
|
snap.rooms[snap.roomCount++] = room(2, "Living");
|
|
snap.rooms[snap.roomCount++] = room(3, "Bed");
|
|
snap.shades[snap.shadeCount++] = shade(1, "Kitchen");
|
|
snap.shades[snap.shadeCount++] = shade(4, "Porch");
|
|
group_snapshot_t g = group(6, "All");
|
|
g.linkedShades[g.linkedShadeCount++] = 4;
|
|
g.linkedShades[g.linkedShadeCount++] = 9; // gone from the model
|
|
g.linkedShades[g.linkedShadeCount++] = 1;
|
|
snap.groups[snap.groupCount++] = g;
|
|
snap.repeaters[snap.repeaterCount++] = 12345;
|
|
snap.repeaters[snap.repeaterCount++] = 678;
|
|
}
|
|
void tearDown() {}
|
|
|
|
/*********************************************************************
|
|
* Items, as the routes have always written them
|
|
********************************************************************/
|
|
static void test_a_shade() {
|
|
snap.shades[0].linkedRemotes[snap.shades[0].linkedRemoteCount++] = { 5555, 7 };
|
|
TEST_ASSERT_EQUAL_STRING(
|
|
"{\"shadeId\":1,\"roomId\":2,\"name\":\"Kitchen\",\"remoteAddress\":1001,\"upTime\":10000,\"downTime\":11000,"
|
|
"\"paired\":true,\"lastRollingCode\":42,\"position\":30,\"tiltType\":2,\"tiltPosition\":40,\"tiltDirection\":0,"
|
|
"\"tiltTime\":7000,\"stepSize\":100,\"tiltTarget\":40,\"target\":30,\"myPos\":-1,\"myTiltPos\":-1,\"direction\":0,"
|
|
"\"shadeType\":1,\"bitLength\":56,\"proto\":0,\"flags\":0,\"flipCommands\":false,\"flipPosition\":false,"
|
|
"\"inGroup\":true,\"sunSensor\":false,\"light\":false,\"repeats\":1,\"sortOrder\":0,\"gpioUp\":0,\"gpioDown\":0,"
|
|
"\"gpioMy\":0,\"gpioLLTrigger\":false,\"simMy\":false,\"linkedRemotes\":[{\"remoteAddress\":5555,\"lastRollingCode\":7}]}",
|
|
shadeJson(1).c_str());
|
|
}
|
|
static void test_a_shade_ref() {
|
|
TEST_ASSERT_EQUAL_STRING(
|
|
"{\"shadeId\":1,\"roomId\":2,\"name\":\"Kitchen\",\"remoteAddress\":1001,\"paired\":true,\"shadeType\":1,"
|
|
"\"flipCommands\":false,\"flipPosition\":false,\"bitLength\":56,\"proto\":0,\"flags\":0,\"sunSensor\":false,"
|
|
"\"hasLight\":false,\"repeats\":1}",
|
|
shadeRefJson(1).c_str());
|
|
}
|
|
static void test_a_room() {
|
|
TEST_ASSERT_EQUAL_STRING("{\"roomId\":2,\"name\":\"Living\",\"sortOrder\":0}", roomJson(2).c_str());
|
|
}
|
|
static void test_a_group_ref() {
|
|
TEST_ASSERT_EQUAL_STRING(
|
|
"{\"groupId\":6,\"roomId\":0,\"name\":\"All\",\"remoteAddress\":2006,\"lastRollingCode\":5,\"bitLength\":80,"
|
|
"\"proto\":0,\"sunSensor\":false,\"flipCommands\":false,\"flags\":0,\"repeats\":2,\"sortOrder\":0}",
|
|
groupRefJson(6).c_str());
|
|
}
|
|
static void test_names_are_escaped() {
|
|
strcpy(snap.rooms[0].name, "A \"big\" room");
|
|
TEST_ASSERT_EQUAL_STRING("{\"roomId\":2,\"name\":\"A \\\"big\\\" room\",\"sortOrder\":0}", roomJson(2).c_str());
|
|
}
|
|
|
|
/*********************************************************************
|
|
* Documents
|
|
********************************************************************/
|
|
static std::string groupWithLinked() {
|
|
std::string g = groupRefJson(6);
|
|
g.pop_back(); // reopen it for linkedShades
|
|
return g + ",\"linkedShades\":[" + shadeRefJson(4) + "," + shadeRefJson(1) + "]}";
|
|
}
|
|
static void test_rooms_list() {
|
|
TEST_ASSERT_EQUAL_STRING(("[" + roomJson(2) + "," + roomJson(3) + "]").c_str(), document(snapshot_doc_t::rooms).c_str());
|
|
}
|
|
static void test_shades_list() {
|
|
TEST_ASSERT_EQUAL_STRING(("[" + shadeJson(1) + "," + shadeJson(4) + "]").c_str(), document(snapshot_doc_t::shades).c_str());
|
|
}
|
|
static void test_groups_list_skips_a_linked_shade_that_is_gone() {
|
|
TEST_ASSERT_EQUAL_STRING(("[" + groupWithLinked() + "]").c_str(), document(snapshot_doc_t::groups).c_str());
|
|
}
|
|
static void test_empty_lists() {
|
|
snap = model_snapshot_t{};
|
|
TEST_ASSERT_EQUAL_STRING("[]", document(snapshot_doc_t::rooms).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("[]", document(snapshot_doc_t::shades).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("[]", document(snapshot_doc_t::groups).c_str());
|
|
}
|
|
static void test_controller() {
|
|
const std::string expected = "{\"startingAddress\":77,\"rooms\":[" + roomJson(2) + "," + roomJson(3) +
|
|
"],\"shades\":[" + shadeJson(1) + "," + shadeJson(4) +
|
|
"],\"groups\":[" + groupWithLinked() +
|
|
"],\"repeaters\":[12345,678]}";
|
|
TEST_ASSERT_EQUAL_STRING(expected.c_str(), document(snapshot_doc_t::controller).c_str());
|
|
}
|
|
static void test_discovery_has_no_repeaters() {
|
|
const std::string expected = "{\"startingAddress\":77,\"rooms\":[" + roomJson(2) + "," + roomJson(3) +
|
|
"],\"shades\":[" + shadeJson(1) + "," + shadeJson(4) +
|
|
"],\"groups\":[" + groupWithLinked() + "]}";
|
|
TEST_ASSERT_EQUAL_STRING(expected.c_str(), document(snapshot_doc_t::discovery).c_str());
|
|
}
|
|
static void test_controller_of_an_empty_model() {
|
|
snap = model_snapshot_t{};
|
|
TEST_ASSERT_EQUAL_STRING("{\"startingAddress\":0,\"rooms\":[],\"shades\":[],\"groups\":[],\"repeaters\":[]}",
|
|
document(snapshot_doc_t::controller).c_str());
|
|
}
|
|
static void test_chunk_size_does_not_change_the_document() {
|
|
const std::string whole = document(snapshot_doc_t::controller, 0, 64);
|
|
for(size_t n = 1; n < 20; n++)
|
|
TEST_ASSERT_EQUAL_STRING(whole.c_str(), document(snapshot_doc_t::controller, 0, n).c_str());
|
|
}
|
|
static void test_single_items() {
|
|
TEST_ASSERT_EQUAL_STRING(roomJson(3).c_str(), document(snapshot_doc_t::room, 3).c_str());
|
|
TEST_ASSERT_EQUAL_STRING(shadeJson(4).c_str(), document(snapshot_doc_t::shade, 4).c_str());
|
|
TEST_ASSERT_EQUAL_STRING(groupWithLinked().c_str(), document(snapshot_doc_t::group, 6).c_str());
|
|
}
|
|
static void test_a_single_item_gone_by_the_snapshot_is_an_error() {
|
|
TEST_ASSERT_EQUAL_STRING("{\"status\":\"ERROR\",\"desc\":\"Shade Id not found.\"}", document(snapshot_doc_t::shade, 9).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}", document(snapshot_doc_t::group, 9).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("{\"status\":\"ERROR\",\"desc\":\"Room Id not found.\"}", document(snapshot_doc_t::room, 9).c_str());
|
|
}
|
|
static void test_an_item_too_big_for_the_unit_is_left_out() {
|
|
// Room JSON is about 40 bytes and a shade's well over 300, so a 200 byte
|
|
// unit takes the rooms and leaves the shades out, keeping the JSON valid.
|
|
TEST_ASSERT_EQUAL_STRING(("[" + roomJson(2) + "," + roomJson(3) + "]").c_str(),
|
|
document(snapshot_doc_t::rooms, 0, 7, 200).c_str());
|
|
TEST_ASSERT_EQUAL_STRING("[]", document(snapshot_doc_t::shades, 0, 7, 200).c_str());
|
|
}
|
|
static void test_a_left_out_item_does_not_leave_a_stray_comma() {
|
|
// Give the first shade every linked remote so it no longer fits a unit the
|
|
// second one does.
|
|
snap.shades[0].linkedRemoteCount = SOMFY_MAX_LINKED_REMOTES;
|
|
for(uint8_t i = 0; i < SOMFY_MAX_LINKED_REMOTES; i++) snap.shades[0].linkedRemotes[i] = { 4000000000u, 65535 };
|
|
const size_t fits = shadeJson(4).length() + 8;
|
|
TEST_ASSERT_TRUE(shadeJson(1).length() > fits);
|
|
TEST_ASSERT_EQUAL_STRING(("[" + shadeJson(4) + "]").c_str(), document(snapshot_doc_t::shades, 0, 7, fits).c_str());
|
|
}
|
|
|
|
/*********************************************************************
|
|
* Repeaters, what a new item would get, and group options
|
|
********************************************************************/
|
|
static void test_repeaters_list() {
|
|
TEST_ASSERT_EQUAL_STRING("[12345,678]", document(snapshot_doc_t::repeaters).c_str());
|
|
}
|
|
static void test_next_room_fills_the_lowest_gap() {
|
|
TEST_ASSERT_EQUAL_STRING("{\"roomId\":1}", document(snapshot_doc_t::nextRoom).c_str());
|
|
}
|
|
static void test_next_shade() {
|
|
// Ids 1 and 4 are taken, so 2; its address is startingAddress + id unless taken.
|
|
snap.radioBitLength = 80;
|
|
snap.radioProto = 1;
|
|
TEST_ASSERT_EQUAL_STRING("{\"shadeId\":2,\"remoteAddress\":79,\"bitLength\":80,\"stepSize\":100,\"proto\":1}",
|
|
document(snapshot_doc_t::nextShade).c_str());
|
|
}
|
|
static void test_next_address_skips_one_already_used() {
|
|
snap.shades[0].remoteAddress = 79; // what shade 2 would get
|
|
snap.shades[1].remoteAddress = 80;
|
|
TEST_ASSERT_EQUAL_UINT32(81, nextRemoteAddress(snap, 2));
|
|
}
|
|
static void test_next_group() {
|
|
TEST_ASSERT_EQUAL_STRING("{\"groupId\":1,\"remoteAddress\":78,\"bitLength\":0,\"proto\":0}",
|
|
document(snapshot_doc_t::nextGroup).c_str());
|
|
}
|
|
static void test_the_last_ids_are_given_out() {
|
|
// Ids used to stop two short: 30 shades where the limit says 32.
|
|
snap.shadeCount = 0;
|
|
for(uint8_t id = 1; id <= SOMFY_MAX_SHADES - 2; id++) snap.shades[snap.shadeCount++] = shade(id, "S");
|
|
TEST_ASSERT_EQUAL_UINT8(SOMFY_MAX_SHADES - 1, nextShadeId(snap));
|
|
snap.shades[snap.shadeCount++] = shade(SOMFY_MAX_SHADES - 1, "S");
|
|
TEST_ASSERT_EQUAL_UINT8(SOMFY_MAX_SHADES, nextShadeId(snap));
|
|
}
|
|
static void test_no_room_for_another_shade() {
|
|
snap.shadeCount = 0;
|
|
for(uint8_t id = 1; id <= SOMFY_MAX_SHADES; id++) snap.shades[snap.shadeCount++] = shade(id, "S");
|
|
TEST_ASSERT_EQUAL_UINT8(255, nextShadeId(snap));
|
|
}
|
|
static void test_group_options_lists_the_shades_not_in_it() {
|
|
snap.shades[snap.shadeCount++] = shade(7, "Spare");
|
|
const std::string expected = groupWithLinked().substr(0, groupWithLinked().length() - 1) +
|
|
",\"availShades\":[" + shadeRefJson(7) + "]}";
|
|
TEST_ASSERT_EQUAL_STRING(expected.c_str(), document(snapshot_doc_t::groupOptions, 6).c_str());
|
|
}
|
|
static void test_group_options_of_a_group_gone_by_the_snapshot() {
|
|
TEST_ASSERT_EQUAL_STRING("{\"status\":\"ERROR\",\"desc\":\"Group Id not found.\"}",
|
|
document(snapshot_doc_t::groupOptions, 9).c_str());
|
|
}
|
|
|
|
/*********************************************************************
|
|
* Slots
|
|
********************************************************************/
|
|
static SnapshotSlots slots;
|
|
static int fills = 0;
|
|
static void countFill(model_snapshot_t &s) {
|
|
fills++;
|
|
s.shadeCount = 1;
|
|
}
|
|
|
|
static void test_two_reads_at_once_and_no_more() {
|
|
const int8_t a = slots.acquire();
|
|
const int8_t b = slots.acquire();
|
|
TEST_ASSERT_TRUE(a >= 0 && b >= 0 && a != b);
|
|
TEST_ASSERT_EQUAL_INT8(-1, slots.acquire());
|
|
slots.cancel(a);
|
|
slots.cancel(b);
|
|
}
|
|
static void test_a_read_is_filled_then_freed() {
|
|
fills = 0;
|
|
const int8_t s = slots.acquire();
|
|
TEST_ASSERT_FALSE(slots.filled(s));
|
|
slots.fill(s, countFill);
|
|
TEST_ASSERT_TRUE(slots.filled(s));
|
|
TEST_ASSERT_EQUAL_UINT8(1, slots.snapshot(s).shadeCount);
|
|
slots.release(s);
|
|
TEST_ASSERT_EQUAL_INT8(s, slots.acquire());
|
|
slots.cancel(s);
|
|
TEST_ASSERT_EQUAL_INT(1, fills);
|
|
}
|
|
static void test_a_read_gone_before_its_fill_is_not_filled() {
|
|
fills = 0;
|
|
const int8_t s = slots.acquire();
|
|
const int8_t other = slots.acquire();
|
|
slots.release(s);
|
|
// Not free yet: loop() still has the fill queued.
|
|
TEST_ASSERT_EQUAL_INT8(-1, slots.acquire());
|
|
slots.fill(s, countFill);
|
|
TEST_ASSERT_EQUAL_INT(0, fills);
|
|
TEST_ASSERT_EQUAL_INT8(s, slots.acquire());
|
|
slots.cancel(s);
|
|
slots.cancel(other);
|
|
}
|
|
static void test_a_cancelled_slot_is_free_again() {
|
|
const int8_t s = slots.acquire();
|
|
slots.cancel(s);
|
|
TEST_ASSERT_EQUAL_INT8(s, slots.acquire());
|
|
slots.cancel(s);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
(void)argc;
|
|
(void)argv;
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_a_shade);
|
|
RUN_TEST(test_a_shade_ref);
|
|
RUN_TEST(test_a_room);
|
|
RUN_TEST(test_a_group_ref);
|
|
RUN_TEST(test_names_are_escaped);
|
|
RUN_TEST(test_rooms_list);
|
|
RUN_TEST(test_shades_list);
|
|
RUN_TEST(test_groups_list_skips_a_linked_shade_that_is_gone);
|
|
RUN_TEST(test_empty_lists);
|
|
RUN_TEST(test_controller);
|
|
RUN_TEST(test_discovery_has_no_repeaters);
|
|
RUN_TEST(test_controller_of_an_empty_model);
|
|
RUN_TEST(test_chunk_size_does_not_change_the_document);
|
|
RUN_TEST(test_single_items);
|
|
RUN_TEST(test_a_single_item_gone_by_the_snapshot_is_an_error);
|
|
RUN_TEST(test_an_item_too_big_for_the_unit_is_left_out);
|
|
RUN_TEST(test_a_left_out_item_does_not_leave_a_stray_comma);
|
|
RUN_TEST(test_repeaters_list);
|
|
RUN_TEST(test_next_room_fills_the_lowest_gap);
|
|
RUN_TEST(test_next_shade);
|
|
RUN_TEST(test_next_address_skips_one_already_used);
|
|
RUN_TEST(test_next_group);
|
|
RUN_TEST(test_the_last_ids_are_given_out);
|
|
RUN_TEST(test_no_room_for_another_shade);
|
|
RUN_TEST(test_group_options_lists_the_shades_not_in_it);
|
|
RUN_TEST(test_group_options_of_a_group_gone_by_the_snapshot);
|
|
RUN_TEST(test_two_reads_at_once_and_no_more);
|
|
RUN_TEST(test_a_read_is_filled_then_freed);
|
|
RUN_TEST(test_a_read_gone_before_its_fill_is_not_filled);
|
|
RUN_TEST(test_a_cancelled_slot_is_free_again);
|
|
return UNITY_END();
|
|
}
|