- CMakeLists в корне: ветвление ESP_PLATFORM (idf_component_register, lwip/esp_timer/esp_hw_support/pthread) / POSIX (статическая библиотека fgl-aircon, C++20, -fno-exceptions -fno-rtti, -Werror). - src/ayla/platform: сокеты/потоки/CSPRNG/время; posix (getrandom, poll, pthread_join) и esp-idf (lwip_select, esp_fill_random, pthread-слой IDF); tcp_shutdown/tcp_local_port/thread_join для управляемой остановки. - src/ayla: log (sink, без printf); мини-httpd/1.1 (keep-alive, Content-Length, лимиты заголовков/тела, ephemeral-порт, жизненный цикл с гарантией завершения потока: shutdown(active)→join→close). - tests/ayla: platform (join, loopback+shutdown) и httpd (404, keep-alive, обработчик/парсинг, oversize-400, stop при живом соединении, стрим заголовков). doctest через FetchContent. - scripts/ci.sh: сборка+ctest. ESP-IDF v5.5.5 esp32: смоук-сборка с ядром как компонентом — Project build complete. Ревью под-агентом: 3 круга, все блокеры (жизненный цикл httpd) закрыты, APPROVED.
109 lines
3.5 KiB
C++
109 lines
3.5 KiB
C++
// Тесты платформенного слоя (POSIX-путь; на ESP-IDF тесты не запускаются —
|
|
// верификация IDF-слоя сборкой и on-device приёмкой).
|
|
#include "doctest/doctest.h"
|
|
|
|
#include <atomic>
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "ayla/platform/platform.hpp"
|
|
|
|
TEST_CASE("now_ms монотонен") {
|
|
uint64_t a = fgl::plat::now_ms();
|
|
fgl::plat::sleep_ms(30);
|
|
uint64_t b = fgl::plat::now_ms();
|
|
CHECK(b >= a + 25);
|
|
CHECK(b - a < 1000);
|
|
}
|
|
|
|
TEST_CASE("random заполняет и не повторяется тривиально") {
|
|
uint8_t a[16] = {}, b[16] = {};
|
|
REQUIRE(fgl::plat::random(a, sizeof(a)));
|
|
REQUIRE(fgl::plat::random(b, sizeof(b)));
|
|
CHECK_FALSE(memcmp(a, b, sizeof(a)) == 0);
|
|
bool all_zero = true;
|
|
for (uint8_t x : a) all_zero = all_zero && (x == 0);
|
|
CHECK_FALSE(all_zero);
|
|
}
|
|
|
|
TEST_CASE("thread_create + thread_join: поток завершён после join") {
|
|
struct Ctx {
|
|
std::atomic<bool> ran{false};
|
|
std::atomic<bool> finished{false};
|
|
};
|
|
Ctx c;
|
|
fgl::plat::ThreadId tid = nullptr;
|
|
REQUIRE(fgl::plat::thread_create(
|
|
[](void* p) {
|
|
auto* x = static_cast<Ctx*>(p);
|
|
x->ran.store(true);
|
|
fgl::plat::sleep_ms(100);
|
|
x->finished.store(true);
|
|
},
|
|
&c, "t_join", 0, &tid));
|
|
REQUIRE(tid != nullptr);
|
|
CHECK_FALSE(c.finished.load());
|
|
fgl::plat::thread_join(tid); // вернётся после завершения потока
|
|
CHECK(c.ran.load());
|
|
CHECK(c.finished.load());
|
|
}
|
|
|
|
TEST_CASE("tcp loopback: ephemeral-порт, connect/send/recv/echo, shutdown") {
|
|
int listen_fd = fgl::plat::tcp_listen(0);
|
|
REQUIRE(listen_fd >= 0);
|
|
const uint16_t port = fgl::plat::tcp_local_port(listen_fd);
|
|
REQUIRE(port != 0);
|
|
|
|
struct SrvCtx {
|
|
int lfd;
|
|
int cfd = -1;
|
|
std::atomic<bool> accepted{false};
|
|
std::atomic<bool> echoed{false};
|
|
std::atomic<uint32_t> peer_ip{0};
|
|
};
|
|
SrvCtx srv;
|
|
srv.lfd = listen_fd;
|
|
fgl::plat::ThreadId tid = nullptr;
|
|
REQUIRE(fgl::plat::thread_create(
|
|
[](void* p) {
|
|
auto* s = static_cast<SrvCtx*>(p);
|
|
uint32_t ip = 0;
|
|
uint16_t peer_port = 0;
|
|
int cfd = fgl::plat::tcp_accept(s->lfd, &ip, &peer_port);
|
|
if (cfd < 0) return;
|
|
s->cfd = cfd;
|
|
s->accepted.store(true);
|
|
s->peer_ip.store(ip);
|
|
fgl::plat::tcp_set_timeout(cfd, 5000, 5000);
|
|
char buf[16];
|
|
long n = fgl::plat::tcp_recv(cfd, buf, sizeof(buf));
|
|
if (n > 0) {
|
|
fgl::plat::tcp_send(cfd, buf, static_cast<size_t>(n));
|
|
s->echoed.store(true);
|
|
// ждём возможного второго запроса (для проверки shutdown ниже)
|
|
char buf2[16];
|
|
fgl::plat::tcp_recv(cfd, buf2, sizeof(buf2));
|
|
}
|
|
fgl::plat::tcp_close(cfd);
|
|
},
|
|
&srv, "t_srv", 0, &tid));
|
|
|
|
int cfd = fgl::plat::tcp_connect("127.0.0.1", port, 2000);
|
|
REQUIRE(cfd >= 0);
|
|
REQUIRE(fgl::plat::tcp_send(cfd, "ping", 4) == 4);
|
|
fgl::plat::tcp_set_timeout(cfd, 2000, 2000);
|
|
char buf[16] = {};
|
|
REQUIRE(fgl::plat::tcp_recv(cfd, buf, sizeof(buf)) == 4);
|
|
CHECK(std::string(buf, 4) == "ping");
|
|
fgl::plat::tcp_close(cfd);
|
|
|
|
REQUIRE(srv.accepted.load());
|
|
CHECK(srv.peer_ip.load() == 0x7f000001); // 127.0.0.1 в host byte order
|
|
REQUIRE(srv.echoed.load());
|
|
|
|
// shutdown прерывает блокированный recv сервера → поток завершается
|
|
fgl::plat::tcp_shutdown(srv.cfd);
|
|
fgl::plat::thread_join(tid);
|
|
fgl::plat::tcp_close(listen_fd);
|
|
}
|