mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
fix: protocol 2 core review findings
From the review of #99: - A sync could stall when a client took a slot the moment another left: loop() used up the new client's sync on the old one. The slots move to ProtoSlots, a pure module with native tests, and loop() takes a slot's wants only for the client it read (takeWants). - Deleting a group sends its shades, whose inGroup changed. Deleting a shade unlinks it from its groups and sends them, so a new shade given its id doesn't join them. - next and sync with an id get an ok reply, and a message in pieces gets one bad request, not one per piece. - Feeds check for loop() before they write the event buffer, and log an event too large, as events do. - editChanges() (ModelEditor, native tests) says what clients get after an edit; the edit result carries the shade a group gained or lost. order() takes a sort_list_t. - The frame writers move to ProtoFrame. Renames: pf to proto_field_t, the X-macro letters to TYPE/FIELD/ERR/FEED, proto_request_t to proto_message_t, REQ_* to WANTS_*, fmt to eventJson. The event bodies share sendItem(), the connection type name lives in ESPNetwork, and the loop() task check in SocketEmitter. - e2e: the heap floor check skips when the low since boot is already under it; tests for replies to next and sync and a long message. - SECURITY_PLAN.md splits step 1 into 1a-1d. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d869bb53d5
commit
dc2f1478b6
29 changed files with 1087 additions and 670 deletions
|
|
@ -35,6 +35,34 @@ def test_an_event_type_from_a_client_is_unknown(connect, schema):
|
|||
assert error_name(schema, reply_to(c, msg_id)) == "unknownOp"
|
||||
|
||||
|
||||
def test_next_and_sync_with_an_id_are_answered(connect, schema):
|
||||
"""A message with an id gets one reply, control messages too."""
|
||||
c = synced(connect, schema)
|
||||
msg_id = c.send(schema.type_numbers["next"], with_id=True)
|
||||
assert reply_to(c, msg_id)["d"].get(schema.keys["ok"]) == 1
|
||||
msg_id = c.send(schema.type_numbers["sync"], with_id=True)
|
||||
assert reply_to(c, msg_id)["d"].get(schema.keys["ok"]) == 1
|
||||
|
||||
|
||||
def test_a_long_message_gets_one_reply(connect, schema):
|
||||
"""A frame bigger than a TCP segment reaches the device in pieces, which it
|
||||
can't put together yet: still one reply for the whole message. The message
|
||||
is wrong either way, so a reply is due however it arrives."""
|
||||
c = synced(connect, schema)
|
||||
c.send_text('{"v":2,"t":%d,"id":5,"x":"%s"}' % (schema.type_numbers["shadeState"], "a" * 3000))
|
||||
replies = []
|
||||
try:
|
||||
while True:
|
||||
f = c.recv(timeout=3)
|
||||
if f["t"] == 2:
|
||||
replies.append(f)
|
||||
except TimeoutError:
|
||||
pass
|
||||
assert len(replies) == 1, replies
|
||||
# In pieces it can't be read, so its id isn't known; whole, it is an event.
|
||||
assert error_name(schema, replies[0]) in ("badRequest", "unknownOp")
|
||||
|
||||
|
||||
def test_subscribing_to_a_feed_is_ok(connect, schema):
|
||||
c = synced(connect, schema)
|
||||
for feed in ("frames", "frequencyScan"):
|
||||
|
|
|
|||
|
|
@ -16,13 +16,17 @@ MAX_CLIENTS = 5
|
|||
|
||||
@pytest.fixture
|
||||
def watch_heap(host):
|
||||
"""Fails the test when the device rebooted or its heap fell under the floor."""
|
||||
"""Fails the test when the device rebooted or its heap fell under the floor.
|
||||
|
||||
The device reports its lowest heap since it booted, so a dip under the
|
||||
floor only shows while that low is still above it."""
|
||||
before = heap(host)
|
||||
if before["min"] < HEAP_FLOOR:
|
||||
pytest.skip("the heap has been down to %u bytes since boot: reboot the device to check the floor" % before["min"])
|
||||
yield before
|
||||
after = heap(host)
|
||||
assert after["uptime"] > before["uptime"], "the device rebooted"
|
||||
assert after["min"] >= HEAP_FLOOR or after["min"] == before["min"], (
|
||||
"the heap fell to %u bytes (floor %u)" % (after["min"], HEAP_FLOOR))
|
||||
assert after["min"] >= HEAP_FLOOR, "the heap fell to %u bytes (floor %u)" % (after["min"], HEAP_FLOOR)
|
||||
|
||||
|
||||
def test_every_client_reconnecting_at_once(connect, schema, watch_heap):
|
||||
|
|
|
|||
|
|
@ -134,6 +134,66 @@ static void test_255_means_no_pin() {
|
|||
TEST_ASSERT_EQUAL_INT8(0, conflict(shade_types::roller, radio_proto::GP_Remote, 255, 26, 255));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* What protocol 2 clients are sent after an edit
|
||||
********************************************************************/
|
||||
static edit_result_t resultFor(uint8_t id, uint8_t shadeId = 0) {
|
||||
edit_result_t r;
|
||||
r.id = id;
|
||||
r.shadeId = shadeId;
|
||||
return r;
|
||||
}
|
||||
static void test_a_shade_edit_sends_the_whole_shade() {
|
||||
const model_edit_type_t types[] = {
|
||||
model_edit_type_t::add_shade, model_edit_type_t::save_shade, model_edit_type_t::set_paired,
|
||||
model_edit_type_t::link_remote, model_edit_type_t::unlink_remote
|
||||
};
|
||||
for(model_edit_type_t t : types) {
|
||||
const edit_changes_t c = editChanges(t, resultFor(7));
|
||||
TEST_ASSERT_EQUAL_UINT8(7, c.shadeId);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, c.groupId);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, c.roomId);
|
||||
}
|
||||
}
|
||||
static void test_a_group_edit_sends_the_whole_group() {
|
||||
TEST_ASSERT_EQUAL_UINT8(3, editChanges(model_edit_type_t::add_group, resultFor(3)).groupId);
|
||||
TEST_ASSERT_EQUAL_UINT8(3, editChanges(model_edit_type_t::save_group, resultFor(3)).groupId);
|
||||
}
|
||||
static void test_linking_a_shade_to_a_group_sends_both() {
|
||||
// The shade's inGroup changes with the group's shades.
|
||||
for(model_edit_type_t t : {model_edit_type_t::link_to_group, model_edit_type_t::unlink_from_group}) {
|
||||
const edit_changes_t c = editChanges(t, resultFor(3, 9));
|
||||
TEST_ASSERT_EQUAL_UINT8(3, c.groupId);
|
||||
TEST_ASSERT_EQUAL_UINT8(9, c.shadeId);
|
||||
}
|
||||
}
|
||||
static void test_a_room_edit_sends_the_room() {
|
||||
TEST_ASSERT_EQUAL_UINT8(2, editChanges(model_edit_type_t::add_room, resultFor(2)).roomId);
|
||||
TEST_ASSERT_EQUAL_UINT8(2, editChanges(model_edit_type_t::save_room, resultFor(2)).roomId);
|
||||
}
|
||||
static void test_a_sort_edit_sends_its_list() {
|
||||
TEST_ASSERT_EQUAL(sort_list_t::rooms, editChanges(model_edit_type_t::room_order, resultFor(0)).order);
|
||||
TEST_ASSERT_EQUAL(sort_list_t::shades, editChanges(model_edit_type_t::shade_order, resultFor(0)).order);
|
||||
TEST_ASSERT_EQUAL(sort_list_t::groups, editChanges(model_edit_type_t::group_order, resultFor(0)).order);
|
||||
TEST_ASSERT_EQUAL(sort_list_t::none, editChanges(model_edit_type_t::save_shade, resultFor(1)).order);
|
||||
}
|
||||
static void test_a_repeater_edit_sends_the_repeaters() {
|
||||
TEST_ASSERT_TRUE(editChanges(model_edit_type_t::link_repeater, resultFor(0)).repeaters);
|
||||
TEST_ASSERT_TRUE(editChanges(model_edit_type_t::unlink_repeater, resultFor(0)).repeaters);
|
||||
TEST_ASSERT_FALSE(editChanges(model_edit_type_t::save_room, resultFor(1)).repeaters);
|
||||
}
|
||||
static void test_deletes_send_nothing_more() {
|
||||
// Their removals go out as they happen.
|
||||
for(model_edit_type_t t : {model_edit_type_t::delete_room, model_edit_type_t::delete_shade, model_edit_type_t::delete_group}) {
|
||||
const edit_changes_t c = editChanges(t, resultFor(4));
|
||||
TEST_ASSERT_EQUAL_UINT8(0, c.shadeId);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, c.groupId);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, c.roomId);
|
||||
TEST_ASSERT_EQUAL(sort_list_t::none, c.order);
|
||||
TEST_ASSERT_FALSE(c.repeaters);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
|
|
@ -157,5 +217,12 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_a_pin_another_shade_uses);
|
||||
RUN_TEST(test_only_the_pins_a_shade_drives_are_checked);
|
||||
RUN_TEST(test_255_means_no_pin);
|
||||
RUN_TEST(test_a_shade_edit_sends_the_whole_shade);
|
||||
RUN_TEST(test_a_group_edit_sends_the_whole_group);
|
||||
RUN_TEST(test_linking_a_shade_to_a_group_sends_both);
|
||||
RUN_TEST(test_a_room_edit_sends_the_room);
|
||||
RUN_TEST(test_a_sort_edit_sends_its_list);
|
||||
RUN_TEST(test_a_repeater_edit_sends_the_repeaters);
|
||||
RUN_TEST(test_deletes_send_nothing_more);
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <vector>
|
||||
#include <ArduinoJson.h>
|
||||
#include "ProtoSchema.h"
|
||||
#include "ProtoFrame.h"
|
||||
#include "ProtoSync.h"
|
||||
#include "ProtoParse.h"
|
||||
|
||||
|
|
@ -21,16 +22,16 @@ static BufferedJsonFormatter fmt;
|
|||
********************************************************************/
|
||||
static void test_field_keys_and_names_are_unique() {
|
||||
std::set<std::string> keys, names;
|
||||
for(uint8_t i = 0; i < static_cast<uint8_t>(pf::count); i++) {
|
||||
const proto_field_info_t &f = protoField(static_cast<pf>(i));
|
||||
for(uint8_t i = 0; i < static_cast<uint8_t>(proto_field_t::count); i++) {
|
||||
const proto_field_info_t &f = protoField(static_cast<proto_field_t>(i));
|
||||
TEST_ASSERT_TRUE_MESSAGE(keys.insert(f.key).second, f.key);
|
||||
TEST_ASSERT_TRUE_MESSAGE(names.insert(f.name).second, f.name);
|
||||
TEST_ASSERT_TRUE_MESSAGE(strlen(f.key) >= 1 && strlen(f.key) <= 2, f.key);
|
||||
}
|
||||
}
|
||||
static void test_every_field_has_a_known_kind() {
|
||||
for(uint8_t i = 0; i < static_cast<uint8_t>(pf::count); i++) {
|
||||
const proto_field_info_t &f = protoField(static_cast<pf>(i));
|
||||
for(uint8_t i = 0; i < static_cast<uint8_t>(proto_field_t::count); i++) {
|
||||
const proto_field_info_t &f = protoField(static_cast<proto_field_t>(i));
|
||||
TEST_ASSERT_NOT_NULL_MESSAGE(strchr("binslo", f.kind), f.name);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,12 +53,12 @@ static void test_error_and_feed_numbers_are_unique() {
|
|||
}
|
||||
static void test_bootstrap_keys_are_fixed() {
|
||||
// A client needs these before it has the schema.
|
||||
TEST_ASSERT_EQUAL_STRING("fw", protoField(pf::fw).key);
|
||||
TEST_ASSERT_EQUAL_STRING("sh", protoField(pf::schemaHash).key);
|
||||
TEST_ASSERT_EQUAL_STRING("T", protoField(pf::types).key);
|
||||
TEST_ASSERT_EQUAL_STRING("F", protoField(pf::fields).key);
|
||||
TEST_ASSERT_EQUAL_STRING("E", protoField(pf::errors).key);
|
||||
TEST_ASSERT_EQUAL_STRING("Q", protoField(pf::feeds).key);
|
||||
TEST_ASSERT_EQUAL_STRING("fw", protoField(proto_field_t::fw).key);
|
||||
TEST_ASSERT_EQUAL_STRING("sh", protoField(proto_field_t::schemaHash).key);
|
||||
TEST_ASSERT_EQUAL_STRING("T", protoField(proto_field_t::types).key);
|
||||
TEST_ASSERT_EQUAL_STRING("F", protoField(proto_field_t::fields).key);
|
||||
TEST_ASSERT_EQUAL_STRING("E", protoField(proto_field_t::errors).key);
|
||||
TEST_ASSERT_EQUAL_STRING("Q", protoField(proto_field_t::feeds).key);
|
||||
}
|
||||
static void test_schema_hash_is_eight_hex_digits_and_stable() {
|
||||
const std::string first = protoSchemaHash();
|
||||
|
|
@ -83,22 +84,22 @@ static std::string writeObject(void (*fill)(ProtoWriter &)) {
|
|||
}
|
||||
static void test_values_at_their_default_are_left_out() {
|
||||
TEST_ASSERT_EQUAL_STRING("{}", writeObject([](ProtoWriter &w) {
|
||||
w.add(pf::position, (int8_t)0);
|
||||
w.add(pf::myPos, (int8_t)-1);
|
||||
w.add(pf::repeats, (uint8_t)1);
|
||||
w.add(pf::paired, false);
|
||||
w.add(pf::name, "");
|
||||
w.add(pf::name, (const char *)nullptr);
|
||||
w.add(proto_field_t::position, (int8_t)0);
|
||||
w.add(proto_field_t::myPos, (int8_t)-1);
|
||||
w.add(proto_field_t::repeats, (uint8_t)1);
|
||||
w.add(proto_field_t::paired, false);
|
||||
w.add(proto_field_t::name, "");
|
||||
w.add(proto_field_t::name, (const char *)nullptr);
|
||||
}).c_str());
|
||||
}
|
||||
static void test_values_off_their_default_are_written_by_key() {
|
||||
TEST_ASSERT_EQUAL_STRING("{\"p\":50,\"m\":0,\"i\":1,\"n\":\"Office\",\"a\":4294967295,\"q\":-5}", writeObject([](ProtoWriter &w) {
|
||||
w.add(pf::position, (int8_t)50);
|
||||
w.add(pf::myPos, (int8_t)0);
|
||||
w.add(pf::paired, true);
|
||||
w.add(pf::name, "Office");
|
||||
w.add(pf::remoteAddress, (uint32_t)0xFFFFFFFF);
|
||||
w.add(pf::tiltPosition, (int8_t)-5);
|
||||
w.add(proto_field_t::position, (int8_t)50);
|
||||
w.add(proto_field_t::myPos, (int8_t)0);
|
||||
w.add(proto_field_t::paired, true);
|
||||
w.add(proto_field_t::name, "Office");
|
||||
w.add(proto_field_t::remoteAddress, (uint32_t)0xFFFFFFFF);
|
||||
w.add(proto_field_t::tiltPosition, (int8_t)-5);
|
||||
}).c_str());
|
||||
}
|
||||
|
||||
|
|
@ -117,6 +118,11 @@ static void test_error_reply_frame() {
|
|||
// ok is 0, its default, so it is left out.
|
||||
TEST_ASSERT_EQUAL_STRING("{\"v\":2,\"t\":2,\"id\":17,\"d\":{\"e\":4,\"x\":\"Shade not found\"}}", buff);
|
||||
}
|
||||
static void test_ok_reply_frame() {
|
||||
fmt.setBuffer(buff, sizeof(buff));
|
||||
writeOkReply(fmt, 9);
|
||||
TEST_ASSERT_EQUAL_STRING("{\"v\":2,\"t\":2,\"id\":9,\"d\":{\"k\":1}}", buff);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Items
|
||||
|
|
@ -193,8 +199,8 @@ class ArraySource : public SyncSource {
|
|||
bool shade(uint8_t slot, shade_snapshot_t &out) override { out = shades[slot]; return out.shadeId != 0; }
|
||||
bool group(uint8_t slot, group_snapshot_t &out) override { out = groups[slot]; return out.groupId != 0; }
|
||||
bool repeater(uint8_t slot, uint32_t &address) override { address = repeaters[slot]; return address != 0; }
|
||||
void header(ProtoWriter &w) override { w.add(pf::maxShades, (uint8_t)SOMFY_MAX_SHADES); }
|
||||
void status(ProtoWriter &w) override { w.add(pf::freeHeap, (uint32_t)80000); }
|
||||
void header(ProtoWriter &w) override { w.add(proto_field_t::maxShades, (uint8_t)SOMFY_MAX_SHADES); }
|
||||
void status(ProtoWriter &w) override { w.add(proto_field_t::freeHeap, (uint32_t)80000); }
|
||||
};
|
||||
static void fillWidest(ArraySource &src) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) {
|
||||
|
|
@ -236,7 +242,7 @@ static std::vector<JsonDocument> runSync(SyncSource &src, bool withSchema, size_
|
|||
return frames;
|
||||
}
|
||||
// How many items a section has over every frame.
|
||||
static size_t count(const std::vector<JsonDocument> &frames, pf section) {
|
||||
static size_t count(const std::vector<JsonDocument> &frames, proto_field_t section) {
|
||||
size_t n = 0;
|
||||
for(const JsonDocument &doc : frames) {
|
||||
JsonVariantConst v = doc["d"][protoField(section).key];
|
||||
|
|
@ -250,16 +256,16 @@ static void test_full_sync_sends_every_item_whole() {
|
|||
fillWidest(src);
|
||||
const std::vector<JsonDocument> frames = runSync(src, true, PROTO_MAX_FRAME);
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(1, frames.size());
|
||||
TEST_ASSERT_EQUAL_UINT32(protoTypeCount(), count(frames, pf::types));
|
||||
TEST_ASSERT_EQUAL_UINT32(static_cast<uint8_t>(pf::count), count(frames, pf::fields));
|
||||
TEST_ASSERT_EQUAL_UINT32(protoErrorCount(), count(frames, pf::errors));
|
||||
TEST_ASSERT_EQUAL_UINT32(protoFeedCount(), count(frames, pf::feeds));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, pf::header));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_ROOMS, count(frames, pf::rooms));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_SHADES, count(frames, pf::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_GROUPS, count(frames, pf::groups));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_REPEATERS, count(frames, pf::repeaters));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, pf::status));
|
||||
TEST_ASSERT_EQUAL_UINT32(protoTypeCount(), count(frames, proto_field_t::types));
|
||||
TEST_ASSERT_EQUAL_UINT32(static_cast<uint8_t>(proto_field_t::count), count(frames, proto_field_t::fields));
|
||||
TEST_ASSERT_EQUAL_UINT32(protoErrorCount(), count(frames, proto_field_t::errors));
|
||||
TEST_ASSERT_EQUAL_UINT32(protoFeedCount(), count(frames, proto_field_t::feeds));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, proto_field_t::header));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_ROOMS, count(frames, proto_field_t::rooms));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_SHADES, count(frames, proto_field_t::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_GROUPS, count(frames, proto_field_t::groups));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_REPEATERS, count(frames, proto_field_t::repeaters));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, proto_field_t::status));
|
||||
// Every shade comes out with all its linked remotes.
|
||||
for(const JsonDocument &doc : frames)
|
||||
for(JsonVariantConst s : doc["d"]["S"].as<JsonArrayConst>())
|
||||
|
|
@ -270,8 +276,8 @@ static void test_a_client_with_the_schema_skips_it() {
|
|||
src = ArraySource();
|
||||
src.shades[0] = restingShade(1);
|
||||
const std::vector<JsonDocument> frames = runSync(src, false, PROTO_MAX_FRAME);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, pf::types));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, pf::fields));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, proto_field_t::types));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, proto_field_t::fields));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, frames.size());
|
||||
TEST_ASSERT_EQUAL(1, frames[0]["d"]["S"][0]["s"].as<int>());
|
||||
}
|
||||
|
|
@ -282,9 +288,9 @@ static void test_empty_slots_are_skipped() {
|
|||
src.shades[20] = restingShade(21);
|
||||
src.rooms[3].roomId = 4;
|
||||
const std::vector<JsonDocument> frames = runSync(src, false, PROTO_MAX_FRAME);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, count(frames, pf::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, pf::rooms));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, pf::groups));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, count(frames, proto_field_t::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, proto_field_t::rooms));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, count(frames, proto_field_t::groups));
|
||||
TEST_ASSERT_TRUE(frames[0]["d"]["X"].isNull());
|
||||
}
|
||||
static void test_a_small_frame_takes_more_frames_and_keeps_items_whole() {
|
||||
|
|
@ -293,8 +299,8 @@ static void test_a_small_frame_takes_more_frames_and_keeps_items_whole() {
|
|||
const std::vector<JsonDocument> big = runSync(src, false, PROTO_MAX_FRAME);
|
||||
const std::vector<JsonDocument> small = runSync(src, false, 1400);
|
||||
TEST_ASSERT_GREATER_THAN_UINT32(big.size(), small.size());
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_SHADES, count(small, pf::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_GROUPS, count(small, pf::groups));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_SHADES, count(small, proto_field_t::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(SOMFY_MAX_GROUPS, count(small, proto_field_t::groups));
|
||||
}
|
||||
static void test_an_item_too_big_for_a_frame_is_left_out_and_the_rest_go() {
|
||||
static ArraySource src;
|
||||
|
|
@ -303,7 +309,7 @@ static void test_an_item_too_big_for_a_frame_is_left_out_and_the_rest_go() {
|
|||
src.shades[1] = restingShade(2);
|
||||
// Too small for the widest shade, big enough for a resting one.
|
||||
const std::vector<JsonDocument> frames = runSync(src, false, 200);
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, pf::shades));
|
||||
TEST_ASSERT_EQUAL_UINT32(1, count(frames, proto_field_t::shades));
|
||||
}
|
||||
static void test_the_largest_shade_fits_one_frame() {
|
||||
// test_full_sync covers it in the sync; this is the shade event on its own.
|
||||
|
|
@ -320,26 +326,26 @@ static void test_the_largest_shade_fits_one_frame() {
|
|||
/*********************************************************************
|
||||
* What clients send
|
||||
********************************************************************/
|
||||
static proto_error_t parse(const char *text, proto_request_t &req) {
|
||||
static proto_error_t parse(const char *text, proto_message_t &req) {
|
||||
JsonDocument doc;
|
||||
return parseClientFrame(text, strlen(text), doc, req);
|
||||
}
|
||||
static void test_next_is_read() {
|
||||
proto_request_t req;
|
||||
proto_message_t req;
|
||||
TEST_ASSERT_EQUAL(proto_error_t::none, parse("{\"v\":2,\"t\":3}", req));
|
||||
TEST_ASSERT_EQUAL_UINT8(static_cast<uint8_t>(proto_type_t::next), req.type);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, req.id);
|
||||
}
|
||||
static void test_subscribe_carries_its_id_and_payload() {
|
||||
proto_request_t req;
|
||||
proto_message_t req;
|
||||
JsonDocument doc;
|
||||
const char *text = "{\"v\":2,\"t\":6,\"id\":9,\"d\":{\"fe\":1}}";
|
||||
TEST_ASSERT_EQUAL(proto_error_t::none, parseClientFrame(text, strlen(text), doc, req));
|
||||
TEST_ASSERT_EQUAL_UINT32(9, req.id);
|
||||
TEST_ASSERT_EQUAL_INT32(1, protoInt(doc["d"], pf::feed));
|
||||
TEST_ASSERT_EQUAL_INT32(1, protoInt(doc["d"], proto_field_t::feed));
|
||||
}
|
||||
static void test_bad_frames_are_refused() {
|
||||
proto_request_t req;
|
||||
proto_message_t req;
|
||||
TEST_ASSERT_EQUAL(proto_error_t::badRequest, parse("not json", req));
|
||||
TEST_ASSERT_EQUAL(proto_error_t::badRequest, parse("[2,3]", req));
|
||||
TEST_ASSERT_EQUAL(proto_error_t::badRequest, parse("{\"v\":1,\"t\":3}", req));
|
||||
|
|
@ -349,7 +355,7 @@ static void test_bad_frames_are_refused() {
|
|||
TEST_ASSERT_EQUAL(proto_error_t::badRequest, parse("{\"v\":2,\"t\":3,\"id\":\"x\"}", req));
|
||||
}
|
||||
static void test_types_a_client_may_not_send_are_unknown() {
|
||||
proto_request_t req;
|
||||
proto_message_t req;
|
||||
// A device event, and a number nobody uses.
|
||||
TEST_ASSERT_EQUAL(proto_error_t::unknownOp, parse("{\"v\":2,\"t\":128,\"id\":5}", req));
|
||||
TEST_ASSERT_EQUAL_UINT32(5, req.id);
|
||||
|
|
@ -359,12 +365,12 @@ static void test_a_left_out_field_reads_as_its_default() {
|
|||
JsonDocument doc;
|
||||
deserializeJson(doc, "{\"p\":40}");
|
||||
JsonObjectConst d = doc.as<JsonObjectConst>();
|
||||
TEST_ASSERT_EQUAL_INT32(40, protoInt(d, pf::position));
|
||||
TEST_ASSERT_EQUAL_INT32(-1, protoInt(d, pf::myPos));
|
||||
TEST_ASSERT_FALSE(protoBool(d, pf::paired));
|
||||
TEST_ASSERT_NULL(protoString(d, pf::name));
|
||||
TEST_ASSERT_TRUE(protoHas(d, pf::position));
|
||||
TEST_ASSERT_FALSE(protoHas(d, pf::target));
|
||||
TEST_ASSERT_EQUAL_INT32(40, protoInt(d, proto_field_t::position));
|
||||
TEST_ASSERT_EQUAL_INT32(-1, protoInt(d, proto_field_t::myPos));
|
||||
TEST_ASSERT_FALSE(protoBool(d, proto_field_t::paired));
|
||||
TEST_ASSERT_NULL(protoString(d, proto_field_t::name));
|
||||
TEST_ASSERT_TRUE(protoHas(d, proto_field_t::position));
|
||||
TEST_ASSERT_FALSE(protoHas(d, proto_field_t::target));
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
|
|
@ -382,6 +388,7 @@ int main(int argc, char **argv) {
|
|||
RUN_TEST(test_values_off_their_default_are_written_by_key);
|
||||
RUN_TEST(test_hello_frame);
|
||||
RUN_TEST(test_error_reply_frame);
|
||||
RUN_TEST(test_ok_reply_frame);
|
||||
RUN_TEST(test_shade_state_of_a_resting_shade_is_its_id);
|
||||
RUN_TEST(test_group_lists_shade_ids);
|
||||
RUN_TEST(test_full_sync_sends_every_item_whole);
|
||||
|
|
|
|||
172
test/test_protoslots/test_protoslots.cpp
Normal file
172
test/test_protoslots/test_protoslots.cpp
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
#include <unity.h>
|
||||
#include "ProtoSlots.h"
|
||||
|
||||
// Protocol 2's client slots and the pace of their syncs: what async_tcp and
|
||||
// loop() each do with a slot, in the orders the two tasks can interleave.
|
||||
|
||||
static proto_slot_t slots[PROTO_MAX_CLIENTS];
|
||||
|
||||
void setUp() {
|
||||
for(proto_slot_t &s : slots) {
|
||||
freeSlot(s);
|
||||
s.loopId = 0;
|
||||
s.cur = sync_cursor_t();
|
||||
s.awaitingNext = false;
|
||||
s.sentAt = 0;
|
||||
}
|
||||
}
|
||||
void tearDown() {}
|
||||
|
||||
// A client as onConnect sets it up: a slot, then hello, then the sync.
|
||||
static proto_slot_t &connect(uint32_t id, bool withSchema = true) {
|
||||
proto_slot_t *slot = claimSlot(slots, PROTO_MAX_CLIENTS, id, withSchema);
|
||||
TEST_ASSERT_NOT_NULL(slot);
|
||||
startSync(*slot);
|
||||
return *slot;
|
||||
}
|
||||
// What sendSyncFrame does with the cursor: on to the end, or one frame in.
|
||||
static void sendAll(proto_slot_t &slot, uint32_t now) {
|
||||
slot.cur.section = sync_section_t::done;
|
||||
syncFrameSent(slot, now);
|
||||
}
|
||||
static void sendSome(proto_slot_t &slot, uint32_t now) {
|
||||
slot.cur.section = sync_section_t::shades;
|
||||
syncFrameSent(slot, now);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Slots
|
||||
********************************************************************/
|
||||
static void test_a_sixth_client_gets_no_slot() {
|
||||
for(uint32_t id = 1; id <= PROTO_MAX_CLIENTS; id++) connect(id);
|
||||
TEST_ASSERT_NULL(claimSlot(slots, PROTO_MAX_CLIENTS, 99, true));
|
||||
}
|
||||
static void test_a_freed_slot_is_taken_again_clean() {
|
||||
proto_slot_t &a = connect(1);
|
||||
a.feeds.fetch_or(0x02);
|
||||
a.wants.fetch_or(WANTS_NEXT);
|
||||
freeSlot(a);
|
||||
TEST_ASSERT_NULL(slotOf(slots, PROTO_MAX_CLIENTS, 1));
|
||||
proto_slot_t *b = claimSlot(slots, PROTO_MAX_CLIENTS, 2, false);
|
||||
TEST_ASSERT_EQUAL_PTR(&a, b);
|
||||
TEST_ASSERT_EQUAL_UINT8(0, b->feeds.load());
|
||||
TEST_ASSERT_EQUAL_UINT32(0, b->wants.load());
|
||||
TEST_ASSERT_FALSE(b->withSchema.load());
|
||||
}
|
||||
static void test_no_sync_before_hello() {
|
||||
// claimSlot alone, as onConnect has it before hello has gone out.
|
||||
proto_slot_t *slot = claimSlot(slots, PROTO_MAX_CLIENTS, 1, true);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(*slot, 0));
|
||||
startSync(*slot);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(*slot, 0));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* The sync's pace
|
||||
********************************************************************/
|
||||
static void test_a_new_client_gets_the_schema_unless_it_has_it() {
|
||||
proto_slot_t &a = connect(1, true);
|
||||
proto_slot_t &b = connect(2, false);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(a, 0));
|
||||
TEST_ASSERT_EQUAL(sync_section_t::types, a.cur.section);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(b, 0));
|
||||
TEST_ASSERT_EQUAL(sync_section_t::header, b.cur.section);
|
||||
TEST_ASSERT_EQUAL_UINT32(2, b.loopId);
|
||||
}
|
||||
static void test_the_next_frame_waits_for_next() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendSome(a, 100);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 200));
|
||||
a.wants.fetch_or(WANTS_NEXT);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(a, 300));
|
||||
}
|
||||
static void test_next_after_the_sync_has_ended_does_nothing() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendAll(a, 100);
|
||||
a.wants.fetch_or(WANTS_NEXT);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 200));
|
||||
}
|
||||
static void test_a_sync_again_starts_over_without_the_schema() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendSome(a, 100);
|
||||
a.wants.fetch_or(WANTS_SYNC_AGAIN);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(a, 200));
|
||||
TEST_ASSERT_EQUAL(sync_section_t::header, a.cur.section);
|
||||
}
|
||||
static void test_no_next_for_30s_closes_the_client_once() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendSome(a, 1000);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 1000 + PROTO_SYNC_TIMEOUT_MS));
|
||||
TEST_ASSERT_EQUAL(sync_step_t::timedOut, nextSyncStep(a, 1001 + PROTO_SYNC_TIMEOUT_MS));
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 2000 + PROTO_SYNC_TIMEOUT_MS));
|
||||
}
|
||||
static void test_a_finished_sync_never_times_out() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendAll(a, 0);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 10 * PROTO_SYNC_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* A slot changing hands under loop()
|
||||
********************************************************************/
|
||||
static void test_a_client_that_left_mid_sync_is_forgotten() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendSome(a, 0);
|
||||
freeSlot(a);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::none, nextSyncStep(a, 1));
|
||||
TEST_ASSERT_FALSE(a.awaitingNext);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, a.loopId);
|
||||
}
|
||||
static void test_the_next_client_in_the_slot_gets_its_own_sync() {
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
sendSome(a, 0);
|
||||
freeSlot(a);
|
||||
connect(2);
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(a, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, a.loopId);
|
||||
TEST_ASSERT_EQUAL(sync_section_t::types, a.cur.section);
|
||||
}
|
||||
static void test_wants_taken_for_a_client_that_left_stay_for_the_next() {
|
||||
// loop() read client 1, then 1 left and 2 took the slot before loop() took
|
||||
// what it wanted: 2's sync must not be used up on 1.
|
||||
proto_slot_t &a = connect(1);
|
||||
nextSyncStep(a, 0);
|
||||
freeSlot(a);
|
||||
connect(2);
|
||||
TEST_ASSERT_EQUAL_UINT32(0, takeWants(a, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(WANTS_SYNC, a.wants.load());
|
||||
TEST_ASSERT_EQUAL(sync_step_t::send, nextSyncStep(a, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(2, a.loopId);
|
||||
}
|
||||
static void test_wants_of_the_client_loop_read_are_taken() {
|
||||
proto_slot_t &a = connect(1);
|
||||
TEST_ASSERT_EQUAL_UINT32(WANTS_SYNC, takeWants(a, 1));
|
||||
TEST_ASSERT_EQUAL_UINT32(0, a.wants.load());
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
UNITY_BEGIN();
|
||||
RUN_TEST(test_a_sixth_client_gets_no_slot);
|
||||
RUN_TEST(test_a_freed_slot_is_taken_again_clean);
|
||||
RUN_TEST(test_no_sync_before_hello);
|
||||
RUN_TEST(test_a_new_client_gets_the_schema_unless_it_has_it);
|
||||
RUN_TEST(test_the_next_frame_waits_for_next);
|
||||
RUN_TEST(test_next_after_the_sync_has_ended_does_nothing);
|
||||
RUN_TEST(test_a_sync_again_starts_over_without_the_schema);
|
||||
RUN_TEST(test_no_next_for_30s_closes_the_client_once);
|
||||
RUN_TEST(test_a_finished_sync_never_times_out);
|
||||
RUN_TEST(test_a_client_that_left_mid_sync_is_forgotten);
|
||||
RUN_TEST(test_the_next_client_in_the_slot_gets_its_own_sync);
|
||||
RUN_TEST(test_wants_taken_for_a_client_that_left_stay_for_the_next);
|
||||
RUN_TEST(test_wants_of_the_client_loop_read_are_taken);
|
||||
return UNITY_END();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue