core(M2): машина состояний сессии Ayla LAN + mock-модуль + интеграционные сценарии
- session.{hpp,cpp}: state machine (idle/registering/online/recovering/
offline/key_error); httpd-обработчики key_exchange (200/426/412, re-key
прозрачно), commands (одна команда, 206/200, envelope, глобальный seq_no),
datapoint (unpack -> PropertyEvent / 401+тишина 50с для re-key-восстановления);
сессионный поток: local_reg POST?dsn/PUT (local_ip_for), keep-alive, backoff
x1.6->60с, 503->offline/NoSlot, activation-timeout->recovering, delete_session
с ожиданием выдачи; очередь с coalescing + batch; телеметрия; колбэки из
двух потоков с задокументированным контрактом; буферы datapoint-пути в Impl.
- platform: local_ip_for (UDP-connect) posix+esp-idf; стек httpd 24576
(переполнение 16КБ поймано gdb на Release).
- mock_ac.py: мок-модуль, stdlib-only чистый python AES-256 (свёрстан с
pycryptodome); сценарии: 503, no-poll, rekey-every, stale-gap (эмуляция
'вернувшегося' приложения), fail-pushes (битая подпись), garbage-pushes
(обрыв блока), break-outbound (исходящий десинк -> модуль ре-кает на
local_reg, как probe1-3), push-every, fail-first-ke.
- session_runner + test_session_mock.py: 9 сценариев через ctest, включая
самосинхронизацию CBC и восстановление после исходящего десинка.
- Прибор AP-WC1E: активация <=1с; re-key семантика ИСПРАВЛЕНА по живым
тестам: re-key при зазоре local_reg >= ~44-50с (не по возрасту сессии!);
при честном keep-alive 15с сессия стабильна без re-key; PROTOCOL/LEGACY/
PLAN обновлены; восстановление = тишина >порога + возврат.
- CI: 7/7 x3 (gcc-Rel, gcc-ASan/UBSan, clang); ESP-IDF esp32 build complete.
Ревью под-агентом: 2 круга (стек httpd, залипание состояний, dangling cfg,
физика десинка) — APPROVED.
This commit is contained in:
135
tests/ayla/session_runner.cpp
Normal file
135
tests/ayla/session_runner.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
// Тестовый раннер сессии: поднимает fgl::ayla::Session против mock_ac.py
|
||||
// (или реального модуля) и печатает события строками в stdout:
|
||||
// STATE <state> <err> — смена состояния
|
||||
// PROP <name> <cmd_id> <status> <kind:value> — push свойства
|
||||
// DELETED — delete_session забран модулем
|
||||
// STATS ... — телеметрия (перед выходом)
|
||||
// Управление окружением:
|
||||
// RUNNER_KEEPALIVE_MS, RUNNER_ACTIVATION_MS, RUNNER_QUIET_MS,
|
||||
// RUNNER_NOSLOT_RETRY_MS, RUNNER_DELETE_WAIT_MS
|
||||
// RUNNER_SET_NAME/VALUE/AT — одиночная SET через AT секунд после старта.
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include "ayla/session.hpp"
|
||||
|
||||
using fgl::ayla::PropertyEvent;
|
||||
using fgl::ayla::Session;
|
||||
using fgl::ayla::SessionError;
|
||||
using fgl::ayla::SessionState;
|
||||
|
||||
static const char* state_name(SessionState st) {
|
||||
switch (st) {
|
||||
case SessionState::kIdle: return "idle";
|
||||
case SessionState::kRegistering: return "registering";
|
||||
case SessionState::kOnline: return "online";
|
||||
case SessionState::kRecovering: return "recovering";
|
||||
case SessionState::kOffline: return "offline";
|
||||
case SessionState::kKeyError: return "key_error";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
static uint32_t env_u32(const char* name, uint32_t def) {
|
||||
const char* v = getenv(name);
|
||||
return v != nullptr ? static_cast<uint32_t>(atoi(v)) : def;
|
||||
}
|
||||
|
||||
static void on_state(void*, SessionState st, SessionError err) {
|
||||
printf("STATE %s %d\n", state_name(st), static_cast<int>(err));
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static void on_property(void*, const PropertyEvent& ev) {
|
||||
if (ev.is_int) {
|
||||
printf("PROP %s %d %d i:%lld\n", ev.name, ev.cmd_id, ev.status,
|
||||
static_cast<long long>(ev.int_value));
|
||||
} else if (ev.is_bool) {
|
||||
printf("PROP %s %d %d b:%d\n", ev.name, ev.cmd_id, ev.status,
|
||||
ev.bool_value ? 1 : 0);
|
||||
} else {
|
||||
printf("PROP %s %d %d s:%s\n", ev.name, ev.cmd_id, ev.status, ev.str_value);
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc < 7) {
|
||||
fprintf(stderr,
|
||||
"usage: %s <host> <device_port> <listen_port> <dsn> <lanip_key> "
|
||||
"<key_id> <duration_sec> [prop ...]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
fgl::ayla::SessionConfig cfg{};
|
||||
cfg.host = argv[1];
|
||||
cfg.device_port = static_cast<uint16_t>(atoi(argv[2]));
|
||||
cfg.listen_port = static_cast<uint16_t>(atoi(argv[3]));
|
||||
cfg.dsn = argv[4];
|
||||
cfg.lanip_key = argv[5];
|
||||
cfg.lanip_key_id = static_cast<uint32_t>(atoi(argv[6]));
|
||||
int duration_sec = atoi(argv[7]);
|
||||
|
||||
fgl::ayla::SessionCallbacks cbs{};
|
||||
cbs.on_state = on_state;
|
||||
cbs.on_property = on_property;
|
||||
|
||||
Session* s = Session::create(cfg, cbs);
|
||||
if (s == nullptr) {
|
||||
fprintf(stderr, "create failed\n");
|
||||
return 2;
|
||||
}
|
||||
fgl::ayla::SessionTimings timings{};
|
||||
timings.keepalive_ms = env_u32("RUNNER_KEEPALIVE_MS", timings.keepalive_ms);
|
||||
timings.activation_timeout_ms =
|
||||
env_u32("RUNNER_ACTIVATION_MS", timings.activation_timeout_ms);
|
||||
timings.recovering_quiet_ms =
|
||||
env_u32("RUNNER_QUIET_MS", timings.recovering_quiet_ms);
|
||||
timings.no_slot_retry_ms = env_u32("RUNNER_NOSLOT_RETRY_MS",
|
||||
timings.no_slot_retry_ms);
|
||||
timings.delete_wait_ms = env_u32("RUNNER_DELETE_WAIT_MS",
|
||||
timings.delete_wait_ms);
|
||||
s->set_timings_for_test(timings);
|
||||
|
||||
if (!s->start()) {
|
||||
fprintf(stderr, "start failed\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Начальная синхронизация: пакет GET всех свойств.
|
||||
if (argc > 8) {
|
||||
s->begin_batch();
|
||||
for (int i = 8; i < argc; i++) {
|
||||
s->get_property(argv[i]);
|
||||
}
|
||||
s->commit_batch();
|
||||
}
|
||||
|
||||
const char* set_name = getenv("RUNNER_SET_NAME");
|
||||
const char* set_val = getenv("RUNNER_SET_VALUE");
|
||||
uint32_t set_at = env_u32("RUNNER_SET_AT", 0);
|
||||
if (set_name != nullptr && set_val != nullptr && set_at > 0) {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(set_at));
|
||||
s->set_property(set_name, atoll(set_val));
|
||||
printf("SET_DONE %s=%s\n", set_name, set_val);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::seconds(duration_sec);
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
s->stop(); // внутри: delete_session + ожидание выдачи
|
||||
printf("DELETED\n");
|
||||
printf("STATS rekeys=%u pushes_ok=%u pushes_bad=%u cmds=%u state=%s\n",
|
||||
s->rekey_count(), s->pushes_ok(), s->pushes_bad(),
|
||||
s->commands_served(), state_name(s->state()));
|
||||
fflush(stdout);
|
||||
delete s;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user