ESPSomfy-RTS/test/test_json/test_json.cpp
cjkas 25d548ef07 fix: send socket events only from loop()
Every socket event is formatted into one static buffer by one shared
formatter.  setConnected() runs on the WiFi/Ethernet event task and sent
memStatus, wifiStrength and ethernet, then ran sockEmit.loop(), while
loop() could be building its own event.  _safecat() checks the room and
then writes at _len, so another task moving _len in between could write
past the end of g_response.

setConnected() now sends nothing.  loop() broadcasts the status every
STATUS_EMIT_MS (30 s): memStatus, then ethernet, or wifiStrength when it
moved 3 dBm or changed channel.  A client that connects still gets all
of it at once from initClients(), so needsBroadcast and the 1.5 s tick
go, and so does the 1.5 s memStatus for the config panel.

SocketEmitter records loop()'s task in begin().  Off it, beginEmit()
logs and hands out a formatter that writes nothing, and endEmit(),
endEmitRoom() and loop() do nothing.

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

255 lines
8.5 KiB
C++

#include <unity.h>
#include "JsonFormatter.h"
// The formatter only gets told about the first BUFF_SIZE bytes. The rest is a
// canary that must never be written.
#define STORAGE_SIZE 160
#define BUFF_SIZE 128
#define CANARY 'X'
static char storage[STORAGE_SIZE];
static BufferedJsonFormatter fmt;
static void useBuffer(size_t size) {
memset(storage, CANARY, sizeof(storage));
fmt.setBuffer(storage, size);
}
static void assertCanary(size_t from) {
for(size_t i = from; i < STORAGE_SIZE; i++) {
char msg[48];
snprintf(msg, sizeof(msg), "wrote past the buffer at byte %u", (unsigned)i);
TEST_ASSERT_EQUAL_HEX8_MESSAGE(CANARY, storage[i], msg);
}
}
// The JSON written for a single top level string value.
static const char *escaped(const char *val) {
useBuffer(BUFF_SIZE);
fmt.addElem(val);
return storage;
}
void setUp() { useBuffer(BUFF_SIZE); }
void tearDown() { assertCanary(BUFF_SIZE); }
/*********************************************************************
* Structure
********************************************************************/
static void test_empty_object() {
fmt.beginObject();
fmt.endObject();
TEST_ASSERT_EQUAL_STRING("{}", storage);
}
static void test_nested_objects_and_arrays() {
fmt.beginObject();
fmt.addElem("a", (uint8_t)1);
fmt.beginObject("b");
fmt.addElem("c", true);
fmt.endObject();
fmt.beginArray("d");
fmt.addElem((uint8_t)1);
fmt.addElem((uint8_t)2);
fmt.beginObject();
fmt.addElem("e", "x");
fmt.endObject();
fmt.endArray();
fmt.endObject();
TEST_ASSERT_EQUAL_STRING("{\"a\":1,\"b\":{\"c\":true},\"d\":[1,2,{\"e\":\"x\"}]}", storage);
}
static void test_array_of_objects() {
fmt.beginArray();
fmt.beginObject();
fmt.endObject();
fmt.beginObject();
fmt.endObject();
fmt.endArray();
TEST_ASSERT_EQUAL_STRING("[{},{}]", storage);
}
static void test_named_object_after_element() {
fmt.beginObject();
fmt.addElem("a", (uint8_t)1);
fmt.beginObject("o");
fmt.endObject();
fmt.endObject();
TEST_ASSERT_EQUAL_STRING("{\"a\":1,\"o\":{}}", storage);
}
/*********************************************************************
* Values
********************************************************************/
static void test_number_and_bool_values() {
fmt.beginArray();
fmt.addElem((int8_t)-5);
fmt.addElem((uint8_t)255);
fmt.addElem((int32_t)-100000);
fmt.addElem((uint32_t)4000000000u);
fmt.addElem(1.5f);
fmt.addElem(true);
fmt.addElem(false);
fmt.endArray();
TEST_ASSERT_EQUAL_STRING("[-5,255,-100000,4000000000,1.5000,true,false]", storage);
}
static void test_named_uint64() {
fmt.beginObject();
fmt.addElem("u", (uint64_t)18446744073709551615ULL);
fmt.endObject();
TEST_ASSERT_EQUAL_STRING("{\"u\":18446744073709551615}", storage);
}
static void test_null_string_is_skipped() {
fmt.beginObject();
fmt.addElem("a", (uint8_t)1);
fmt.addElem("n", (const char *)nullptr);
fmt.addElem("b", (uint8_t)2);
fmt.endObject();
TEST_ASSERT_EQUAL_STRING("{\"a\":1,\"b\":2}", storage);
}
/*********************************************************************
* Escaping
********************************************************************/
static void test_escapes_special_characters() {
TEST_ASSERT_EQUAL_STRING("\"plain\"", escaped("plain"));
TEST_ASSERT_EQUAL_STRING("\"\\\"\"", escaped("\""));
TEST_ASSERT_EQUAL_STRING("\"\\\\\"", escaped("\\"));
TEST_ASSERT_EQUAL_STRING("\"\\/\"", escaped("/"));
TEST_ASSERT_EQUAL_STRING("\"\\n\\t\\r\\b\\f\"", escaped("\n\t\r\b\f"));
TEST_ASSERT_EQUAL_STRING("\"Living \\\"Room\\\"\"", escaped("Living \"Room\""));
}
static void test_escapes_other_control_characters() {
// Raw control characters are not allowed in JSON strings.
TEST_ASSERT_EQUAL_STRING("\"a\\u0001b\"", escaped("a\x01" "b"));
TEST_ASSERT_EQUAL_STRING("\"\\u001f\"", escaped("\x1f"));
}
static void test_escaped_length() {
TEST_ASSERT_EQUAL_UINT32(0, fmt.calcEscapedLength(""));
TEST_ASSERT_EQUAL_UINT32(2, fmt.calcEscapedLength("ab"));
TEST_ASSERT_EQUAL_UINT32(4, fmt.calcEscapedLength("a\"b"));
}
static void test_escaped_length_counts_first_character() {
TEST_ASSERT_EQUAL_UINT32(4, fmt.calcEscapedLength("\"ab"));
TEST_ASSERT_EQUAL_UINT32(2, fmt.calcEscapedLength("/"));
}
/*********************************************************************
* Buffer limits
********************************************************************/
static void test_value_needing_one_byte_too_many_is_refused() {
// "\/abcdefg" with its quotes is 11 characters, so with the terminator it
// needs 12 bytes and must not be written into 11.
useBuffer(11);
fmt.addElem("/abcdefg");
TEST_ASSERT_TRUE(strlen(storage) < 11);
assertCanary(11);
}
static void test_overflow_is_flagged() {
useBuffer(16);
fmt.beginObject();
fmt.addElem("name", "fits");
TEST_ASSERT_FALSE(fmt.overflowed());
fmt.addElem("long", "this value does not fit");
TEST_ASSERT_TRUE(fmt.overflowed());
// What was already written (here up to the comma) stays intact and terminated.
TEST_ASSERT_EQUAL_STRING("{\"name\":\"fits\",", storage);
assertCanary(16);
}
static void test_setBuffer_clears_overflow() {
useBuffer(4);
fmt.addElem("too long");
TEST_ASSERT_TRUE(fmt.overflowed());
useBuffer(BUFF_SIZE);
TEST_ASSERT_FALSE(fmt.overflowed());
}
/*********************************************************************
* Tracked length
********************************************************************/
static void test_length_matches_what_was_written() {
fmt.beginObject();
fmt.addElem("name", "Living \"Room\"\n");
fmt.addElem("pos", 41.5f);
fmt.beginArray("ids");
fmt.addElem((uint8_t)1);
fmt.addElem((int32_t)-2);
fmt.endArray();
fmt.addElem("on", true);
fmt.endObject();
TEST_ASSERT_EQUAL_UINT32(strlen(storage), fmt.length());
TEST_ASSERT_EQUAL_STRING("{\"name\":\"Living \\\"Room\\\"\\n\",\"pos\":41.5000,\"ids\":[1,-2],\"on\":true}", storage);
}
static void test_refused_append_leaves_length_alone() {
useBuffer(16);
fmt.beginObject();
fmt.addElem("name", "fits");
const size_t before = fmt.length();
fmt.addElem("x", "this value does not fit");
TEST_ASSERT_TRUE(fmt.overflowed());
// The comma and key still fit; the value was refused whole.
TEST_ASSERT_EQUAL_UINT32(strlen(storage), fmt.length());
TEST_ASSERT_TRUE(fmt.length() >= before);
TEST_ASSERT_TRUE(fmt.length() < 16);
}
static void test_setBuffer_resets_length() {
fmt.addElem("something");
TEST_ASSERT_TRUE(fmt.length() > 0);
useBuffer(BUFF_SIZE);
TEST_ASSERT_EQUAL_UINT32(0, fmt.length());
TEST_ASSERT_EQUAL_STRING("", storage);
}
/*********************************************************************
* discard
********************************************************************/
static void test_discard_writes_nothing() {
// What a socket event built off loop() goes into. The buffer the formatter
// had before must not see a byte of it.
fmt.beginObject();
fmt.addElem("kept", true);
char before[STORAGE_SIZE];
memcpy(before, storage, sizeof(before));
fmt.discard();
fmt.beginObject();
fmt.addElem("ssid", "Home \"WiFi\"");
fmt.addElem("strength", (int32_t)-70);
fmt.beginArray("pulses");
fmt.addElem((uint32_t)2416);
fmt.endArray();
fmt.addElem("big", (uint64_t)18446744073709551615ULL);
fmt.endObject();
TEST_ASSERT_EQUAL_MEMORY(before, storage, sizeof(before));
TEST_ASSERT_EQUAL_UINT32(0, fmt.length());
TEST_ASSERT_TRUE(fmt.overflowed());
}
static void test_setBuffer_after_discard_writes_again() {
fmt.discard();
fmt.addElem("dropped");
useBuffer(BUFF_SIZE);
fmt.beginObject();
fmt.endObject();
TEST_ASSERT_FALSE(fmt.overflowed());
TEST_ASSERT_EQUAL_STRING("{}", storage);
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
UNITY_BEGIN();
RUN_TEST(test_empty_object);
RUN_TEST(test_nested_objects_and_arrays);
RUN_TEST(test_array_of_objects);
RUN_TEST(test_named_object_after_element);
RUN_TEST(test_number_and_bool_values);
RUN_TEST(test_named_uint64);
RUN_TEST(test_null_string_is_skipped);
RUN_TEST(test_escapes_special_characters);
RUN_TEST(test_escapes_other_control_characters);
RUN_TEST(test_escaped_length);
RUN_TEST(test_escaped_length_counts_first_character);
RUN_TEST(test_value_needing_one_byte_too_many_is_refused);
RUN_TEST(test_overflow_is_flagged);
RUN_TEST(test_setBuffer_clears_overflow);
RUN_TEST(test_length_matches_what_was_written);
RUN_TEST(test_refused_append_leaves_length_alone);
RUN_TEST(test_setBuffer_resets_length);
RUN_TEST(test_discard_writes_nothing);
RUN_TEST(test_setBuffer_after_discard_writes_again);
return UNITY_END();
}