ESPSomfy-RTS/test/test_protoassembly/test_protoassembly.cpp
cjkas 8fa087aa89 fix: edit ops review findings
From the review of 7723711:
- The assembler is locked: a client closed for a full queue is closed
  by the task that was sending, loop(), whose disconnect dropped the
  buffer while async_tcp could be copying into it. A whole message is
  taken out of it, so a disconnect can't free it while it is read.
- loop() expires a message stalled for 10 s, freeing its buffer and
  telling its client, instead of holding it until another client came.
  A binary message is refused before it can touch another's buffer.
- Each edit takes only the fields its route reads; one it would ignore
  is refused by name. linkRemote's rolling code goes in under the
  route's name, so applyModelEdit is unchanged again.
- An edit refused for an id that doesn't exist replies not found
  (edit_result_t::missing). A reply that doesn't fit its buffer goes
  as an internal error, so the op still gets one.
- The TYPE table says what becomes of each op (proto_op_t), which the
  socket switches on. The reply address is one proto_reply_to_t. The
  routes and the ops start and queue an edit the same way
  (newModelEdit, submitModelEdit). The full-list texts and the offered
  remote address rule live in ModelEditor.
- e2e: the sort order test skips a list that it would renumber.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 18:10:06 +02:00

120 lines
4.7 KiB
C++

#include <unity.h>
#include <stdlib.h>
#include <string.h>
#include "ProtoAssembly.h"
// Putting protocol 2 messages back together from their pieces: one message at
// a time, one reply-worthy result per message.
void setUp() {}
void tearDown() {}
static proto_piece_t piece(uint32_t client, const char *text, bool first, bool last, bool isText = true) {
proto_piece_t p;
p.clientId = client;
p.first = first;
p.last = last;
p.text = isText;
p.data = reinterpret_cast<const uint8_t *>(text);
p.len = strlen(text);
return p;
}
// Takes the complete message and checks it.
static void assertTaken(MessageAssembler &a, const char *expected) {
size_t len = 0;
char *message = a.take(len);
TEST_ASSERT_EQUAL_STRING(expected, message);
TEST_ASSERT_EQUAL_UINT32(strlen(expected), len);
free(message);
}
static void test_pieces_make_the_message() {
MessageAssembler a(64);
TEST_ASSERT_EQUAL(assembly_t::more, a.add(piece(1, "{\"v\":2,", true, false), 0));
TEST_ASSERT_EQUAL(assembly_t::more, a.add(piece(1, "\"t\":3", false, false), 0));
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(1, "}", false, true), 0));
assertTaken(a, "{\"v\":2,\"t\":3}");
}
static void test_nothing_is_held_once_the_message_is_taken() {
MessageAssembler a(64);
TEST_ASSERT_FALSE(a.holding());
a.add(piece(1, "ab", true, false), 0);
TEST_ASSERT_TRUE(a.holding());
a.add(piece(1, "c", false, true), 0);
assertTaken(a, "abc");
TEST_ASSERT_FALSE(a.holding());
// A client leaving now can't free what was taken.
a.drop(1);
}
static void test_another_clients_message_waits() {
MessageAssembler a(64);
a.add(piece(1, "ab", true, false), 0);
// One busy for the other client's message, then its pieces are let go.
TEST_ASSERT_EQUAL(assembly_t::busy, a.add(piece(2, "xy", true, false), 10));
TEST_ASSERT_EQUAL(assembly_t::ignored, a.add(piece(2, "z", false, true), 10));
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(1, "c", false, true), 20));
assertTaken(a, "abc");
}
static void test_too_large_is_said_once() {
MessageAssembler a(4);
TEST_ASSERT_EQUAL(assembly_t::more, a.add(piece(1, "abc", true, false), 0));
TEST_ASSERT_EQUAL(assembly_t::tooLarge, a.add(piece(1, "de", false, false), 0));
TEST_ASSERT_EQUAL(assembly_t::ignored, a.add(piece(1, "f", false, true), 0));
TEST_ASSERT_FALSE(a.holding());
}
static void test_a_binary_message_is_refused_once() {
MessageAssembler a(64);
TEST_ASSERT_EQUAL(assembly_t::notText, a.add(piece(1, "ab", true, false, false), 0));
TEST_ASSERT_EQUAL(assembly_t::ignored, a.add(piece(1, "c", false, true, false), 0));
}
static void test_a_binary_message_leaves_another_clients_alone() {
MessageAssembler a(64);
a.add(piece(1, "ab", true, false), 0);
TEST_ASSERT_EQUAL(assembly_t::notText, a.add(piece(2, "x", true, true, false), 0));
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(1, "c", false, true), 0));
assertTaken(a, "abc");
}
static void test_a_stalled_message_expires_and_its_client_is_owed_a_reply() {
MessageAssembler a(64);
a.add(piece(1, "ab", true, false), 0);
TEST_ASSERT_EQUAL_UINT32(0, a.expire(PROTO_ASSEMBLY_TIMEOUT_MS - 1));
TEST_ASSERT_TRUE(a.holding());
TEST_ASSERT_EQUAL_UINT32(1, a.expire(PROTO_ASSEMBLY_TIMEOUT_MS));
TEST_ASSERT_FALSE(a.holding());
TEST_ASSERT_EQUAL_UINT32(0, a.expire(2 * PROTO_ASSEMBLY_TIMEOUT_MS));
// Its late pieces were answered by the expiry.
TEST_ASSERT_EQUAL(assembly_t::ignored, a.add(piece(1, "c", false, true), PROTO_ASSEMBLY_TIMEOUT_MS + 1));
// And the buffer is free for anyone.
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(2, "xy", true, true), PROTO_ASSEMBLY_TIMEOUT_MS + 1));
assertTaken(a, "xy");
}
static void test_a_client_that_leaves_lets_go() {
MessageAssembler a(64);
a.add(piece(1, "ab", true, false), 0);
a.drop(1);
TEST_ASSERT_FALSE(a.holding());
TEST_ASSERT_EQUAL_UINT32(0, a.expire(PROTO_ASSEMBLY_TIMEOUT_MS));
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(2, "xy", true, true), 1));
assertTaken(a, "xy");
}
static void test_a_message_of_one_piece_is_whole() {
MessageAssembler a(64);
TEST_ASSERT_EQUAL(assembly_t::complete, a.add(piece(1, "{}", true, true), 0));
assertTaken(a, "{}");
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_pieces_make_the_message);
RUN_TEST(test_nothing_is_held_once_the_message_is_taken);
RUN_TEST(test_another_clients_message_waits);
RUN_TEST(test_too_large_is_said_once);
RUN_TEST(test_a_binary_message_is_refused_once);
RUN_TEST(test_a_binary_message_leaves_another_clients_alone);
RUN_TEST(test_a_stalled_message_expires_and_its_client_is_owed_a_reply);
RUN_TEST(test_a_client_that_leaves_lets_go);
RUN_TEST(test_a_message_of_one_piece_is_whole);
return UNITY_END();
}