Files
homeagent-sdk/remotedevice/test/test_ha_remotedevice.c
JianFeeeee fc876c5554 feat: 单插件重载等 PluginMgr 能力导出到外部 SDK
- meta: CORE_PLUGIN_RELOAD_ONE(48) / LIST_LOADED(49) / IS_DISABLED(50)
- sdk: PluginMgrAPI 接口(ReloadOne/ListLoadedPlugins/IsPluginDisabled) +
  PluginSDK.SetPluginMgrAPI/PluginMgr() 访问器
- plugindev 模板: dispatchPluginMgr 桥接注入,走 C ABI 48/49/50
2026-08-23 19:47:55 +08:00

1509 lines
46 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* ha_remotedevice 全面测试
*
* 覆盖JSON 解析/构建、WS 帧编解码、工具函数、
* 客户端完整生命周期、命令分发、二进制分块、语音数据、事件上报。
*
* 编译 (POSIX/Linux/macOS):
* gcc -I../include -I../src ../src/*.c test_ha_remotedevice.c -lpthread -o test
*
* 编译 (Windows/MinGW):
* gcc -I../include -I../src ../src/*.c test_ha_remotedevice.c -lpthread -lws2_32 -o test
*/
#include "ha_remotedevice.h"
#include "ha_json.h"
#include "ha_ws.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <time.h>
/* 前向声明ha_client_send_data_chunked 供测试调用 */
void ha_client_send_data_chunked(ha_client_t *client, const char *req_id,
const char *kind, const char *mime,
const uint8_t *data, int len);
/* ==================================================================
* 平台适配
* ================================================================== */
#if defined(_WIN32) || defined(_WIN64)
#define _CRT_SECURE_NO_WARNINGS
#include <winsock2.h>
#include <windows.h>
#include <process.h>
typedef int socklen_t;
#define sleep(t) Sleep((t)*1000)
#define usleep(t) Sleep((t)/1000)
typedef unsigned thread_func_ret;
#define THREAD_RETURN return 0
#define SIGPIPE 13
static void signal(int sig, void (*func)(int)) { (void)sig; (void)func; }
static void sock_close(int fd) { closesocket(fd); }
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
typedef void *thread_func_ret;
#define THREAD_RETURN return NULL
static void sock_close(int fd) { close(fd); }
#endif
/* ==================================================================
* 简易测试框架
* ================================================================== */
static int tests_passed = 0;
static int tests_failed = 0;
static int tests_skipped = 0;
#define TEST_BEGIN(name) do { \
printf(" TEST: %s ... ", name); \
fflush(stdout); \
do { (void)0
#define TEST_END() } while(0); \
printf("PASS\n"); \
tests_passed++; \
} while(0)
#define TEST_FAIL(msg) do { \
printf("FAIL: %s\n", msg); \
tests_failed++; \
return; \
} while(0)
#define TEST_ASSERT(cond, msg) do { \
if (!(cond)) { TEST_FAIL(msg); } \
} while(0)
#define TEST_SKIP(reason) do { \
printf("SKIP: %s\n", reason); \
tests_skipped++; \
return; \
} while(0)
/* ==================================================================
* 工具:在本地端口启动一个 TCP 服务器(用于 mock 网关)
* ================================================================== */
typedef struct {
int listen_fd;
int client_fd;
int port;
#if defined(_WIN32) || defined(_WIN64)
uintptr_t thread;
#else
pthread_t thread;
#endif
volatile int running;
} mock_server_t;
/* 发送 WS 文本帧(服务器端,无需掩码) */
static void mock_send_text(int fd, const char *json) {
int len = (int)strlen(json);
uint8_t hdr[10];
int hdr_len = 2;
hdr[0] = 0x80 | 0x1;
if (len < 126) {
hdr[1] = (uint8_t)len;
} else if (len < 65536) {
hdr[1] = 126;
hdr[2] = (uint8_t)(len >> 8);
hdr[3] = (uint8_t)(len & 0xFF);
hdr_len = 4;
} else {
hdr[1] = 127;
uint64_t l = (uint64_t)len;
for (int i = 8; i > 0; i--) {
hdr[1 + i] = (uint8_t)(l & 0xFF);
l >>= 8;
}
hdr_len = 10;
}
send(fd, (const char *)hdr, hdr_len, 0);
send(fd, json, len, 0);
}
/* 发送 WS 二进制帧(服务器端,无需掩码) */
static void mock_send_binary(int fd, const uint8_t *data, int len) {
uint8_t hdr[10];
int hdr_len = 2;
hdr[0] = 0x80 | 0x2;
if (len < 126) {
hdr[1] = (uint8_t)len;
} else if (len < 65536) {
hdr[1] = 126;
hdr[2] = (uint8_t)(len >> 8);
hdr[3] = (uint8_t)(len & 0xFF);
hdr_len = 4;
} else {
hdr[1] = 127;
uint64_t l = (uint64_t)len;
for (int i = 8; i > 0; i--) {
hdr[1 + i] = (uint8_t)(l & 0xFF);
l >>= 8;
}
hdr_len = 10;
}
send(fd, (const char *)hdr, hdr_len, 0);
send(fd, (const char *)data, len, 0);
}
/* 读取 WS 文本帧(服务器端,解析掩码) */
static char *mock_read_text(int fd) {
uint8_t hdr[2];
if (recv(fd, (char *)hdr, 2, 0) != 2) return NULL;
int masked = (hdr[1] & 0x80) ? 1 : 0;
uint64_t len = hdr[1] & 0x7F;
if (len == 126) {
uint8_t ext[2];
if (recv(fd, (char *)ext, 2, 0) != 2) return NULL;
len = ((uint64_t)ext[0] << 8) | ext[1];
} else if (len == 127) {
uint8_t ext[8];
if (recv(fd, (char *)ext, 8, 0) != 8) return NULL;
len = 0;
for (int i = 0; i < 8; i++) len = (len << 8) | ext[i];
}
uint8_t mask_key[4] = {0};
if (masked) {
if (recv(fd, (char *)mask_key, 4, 0) != 4) return NULL;
}
char *buf = (char *)malloc((size_t)len + 1);
if (!buf) return NULL;
if (len > 0) {
if ((int)recv(fd, buf, (int)len, 0) != (int)len) {
free(buf);
return NULL;
}
if (masked) {
for (uint64_t i = 0; i < len; i++)
buf[i] ^= mask_key[i & 3];
}
}
buf[len] = '\0';
return buf;
}
/* mock 服务器线程 */
#if defined(_WIN32) || defined(_WIN64)
static unsigned __stdcall mock_server_thread(void *arg) {
#else
static void *mock_server_thread(void *arg) {
#endif
mock_server_t *ms = (mock_server_t *)arg;
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int fd = accept(ms->listen_fd, (struct sockaddr *)&client_addr, &addr_len);
if (fd < 0) THREAD_RETURN;
ms->client_fd = fd;
/* 读取 WS 升级请求 */
char buf[4096] = {0};
int n = 0;
while (n < (int)sizeof(buf) - 1) {
int r = (int)recv(fd, buf + n, 1, 0);
if (r <= 0) break;
n += r;
buf[n] = '\0';
if (n >= 4 && strcmp(buf + n - 4, "\r\n\r\n") == 0) break;
}
/* 发送 101 响应 */
const char *resp =
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n"
"\r\n";
send(fd, resp, (int)strlen(resp), 0);
/* 协议循环 */
char *msg;
while (ms->running) {
msg = mock_read_text(fd);
if (!msg) break;
if (strstr(msg, "\"hello\"")) {
mock_send_text(fd, "{\"op\":\"hello_ack\",\"status\":\"ok\"}");
} else if (strstr(msg, "\"bind\"")) {
mock_send_text(fd, "{\"op\":\"bind_ack\",\"status\":\"ok\",\"device_id\":\"test-dev\"}");
}
/* cmd_result, event, status, cmd_data_start/end 可以忽略 */
free(msg);
}
sock_close(fd);
ms->client_fd = -1;
THREAD_RETURN;
}
static mock_server_t *mock_server_start(int port) {
mock_server_t *ms = (mock_server_t *)calloc(1, sizeof(mock_server_t));
#if defined(_WIN32) || defined(_WIN64)
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
#endif
ms->listen_fd = (int)socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(ms->listen_fd, SOL_SOCKET, SO_REUSEADDR,
(const char *)&opt, sizeof(opt));
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
addr.sin_port = htons(port);
if (bind(ms->listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
free(ms);
return NULL;
}
listen(ms->listen_fd, 1);
ms->port = port;
ms->running = 1;
ms->client_fd = -1;
#if defined(_WIN32) || defined(_WIN64)
ms->thread = _beginthreadex(NULL, 0, mock_server_thread, ms, 0, NULL);
#else
pthread_create(&ms->thread, NULL, mock_server_thread, ms);
#endif
usleep(200000); /* 等待服务器就绪 */
return ms;
}
static void mock_server_stop(mock_server_t *ms) {
if (!ms) return;
ms->running = 0;
if (ms->client_fd >= 0) sock_close(ms->client_fd);
sock_close(ms->listen_fd);
#if defined(_WIN32) || defined(_WIN64)
WaitForSingleObject((HANDLE)ms->thread, 3000);
WSACleanup();
#else
pthread_join(ms->thread, NULL);
#endif
free(ms);
}
/* ==================================================================
* 传输层实现POSIX socket
* ================================================================== */
struct transport_ctx {
int sock;
};
static int transport_connect(void *ctx, const char *host, uint16_t port) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
struct hostent *he = gethostbyname(host);
if (!he) return -1;
tc->sock = (int)socket(AF_INET, SOCK_STREAM, 0);
if (tc->sock < 0) return -1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
if (connect(tc->sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
sock_close(tc->sock);
tc->sock = -1;
return -1;
}
/* 设置 100ms 接收超时,使 ha_client_process 不会永久阻塞 */
#if defined(_WIN32) || defined(_WIN64)
DWORD timeout = 100;
setsockopt(tc->sock, SOL_SOCKET, SO_RCVTIMEO, (const char *)&timeout, sizeof(timeout));
#else
struct timeval tv = {0, 100000};
setsockopt(tc->sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
#endif
return 0;
}
static int transport_send(void *ctx, const uint8_t *data, int len) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
int sent = 0;
while (sent < len) {
int n = (int)send(tc->sock, (const char *)(data + sent), len - sent, 0);
if (n <= 0) return -1;
sent += n;
}
return sent;
}
static int transport_recv(void *ctx, uint8_t *buf, int len) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
return (int)recv(tc->sock, (char *)buf, len, 0);
}
static void transport_close(void *ctx) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
if (tc->sock >= 0) {
sock_close(tc->sock);
tc->sock = -1;
}
}
/* ==================================================================
* mock 服务器:发送命令
* ================================================================== */
static void mock_send_cmd(mock_server_t *ms, const char *cmd_type,
const char *req_id, const char *cmd) {
char buf[4096];
char escaped[2048];
int ei = 0;
/* 转义 cmd 中的双引号和反斜杠 */
if (cmd) {
for (int i = 0; cmd[i] && ei < (int)sizeof(escaped) - 6; i++) {
if (cmd[i] == '"' || cmd[i] == '\\') {
escaped[ei++] = '\\';
if (ei >= (int)sizeof(escaped) - 1) break;
}
escaped[ei++] = cmd[i];
}
}
escaped[ei] = '\0';
int n = snprintf(buf, sizeof(buf),
"{\"op\":\"cmd\",\"req_id\":\"%s\",\"cmd_type\":\"%s\",\"command\":\"%s\"}",
req_id ? req_id : "", cmd_type ? cmd_type : "", escaped);
mock_send_text(ms->client_fd, buf);
}
/* ==================================================================
* 测试用例
* ================================================================== */
/* ---------- 1. JSON 解析器 ---------- */
static void test_json_parser(void) {
TEST_BEGIN("JSON parser: parse object");
const char *json = "{\"op\":\"hello\",\"device\":{\"id\":\"test\",\"caps\":[\"a\",\"b\"]}}";
ha_json_node_t *root = ha_json_parse(json);
TEST_ASSERT(root != NULL, "parse failed");
TEST_ASSERT(root->type == HA_JSON_OBJECT, "not an object");
const char *op = ha_json_get_string(root, "op");
TEST_ASSERT(op != NULL && strcmp(op, "hello") == 0, "op mismatch");
ha_json_node_t *dev = ha_json_get(root, "device");
TEST_ASSERT(dev != NULL && dev->type == HA_JSON_OBJECT, "device not object");
const char *id = ha_json_get_string(dev, "id");
TEST_ASSERT(id != NULL && strcmp(id, "test") == 0, "device id mismatch");
ha_json_node_t *caps = ha_json_get(dev, "caps");
TEST_ASSERT(caps != NULL && caps->type == HA_JSON_ARRAY, "caps not array");
TEST_ASSERT(ha_json_array_len(caps) == 2, "caps length wrong");
ha_json_node_t *c0 = ha_json_array_get(caps, 0);
TEST_ASSERT(c0 != NULL && c0->type == HA_JSON_STRING &&
strcmp(c0->str_val, "a") == 0, "caps[0] mismatch");
ha_json_free(root);
TEST_END();
}
static void test_json_int(void) {
TEST_BEGIN("JSON parser: int fields");
const char *json = "{\"count\":42,\"total\":100500,\"neg\":-7}";
ha_json_node_t *root = ha_json_parse(json);
TEST_ASSERT(root != NULL, "parse failed");
TEST_ASSERT(ha_json_get_int(root, "count", -1) == 42, "count mismatch");
TEST_ASSERT(ha_json_get_int(root, "total", -1) == 100500, "total mismatch");
TEST_ASSERT(ha_json_get_int(root, "neg", 0) == -7, "neg mismatch");
TEST_ASSERT(ha_json_get_int(root, "nonexistent", -999) == -999, "default wrong");
ha_json_free(root);
TEST_END();
}
static void test_json_builder(void) {
TEST_BEGIN("JSON builder: object with string + int + bool");
char buf[256];
ha_json_builder_t jb;
ha_json_builder_init(&jb, buf, sizeof(buf));
ha_json_builder_begin_object(&jb);
ha_json_builder_string(&jb, "op", "hello");
ha_json_builder_int(&jb, "seq", 1);
ha_json_builder_bool(&jb, "active", 1);
ha_json_builder_end_object(&jb);
const char *r = ha_json_builder_str(&jb);
TEST_ASSERT(r != NULL, "builder returned NULL");
TEST_ASSERT(strstr(r, "\"op\":\"hello\"") != NULL, "missing op");
TEST_ASSERT(strstr(r, "\"seq\":1") != NULL, "missing seq");
TEST_ASSERT(strstr(r, "\"active\":true") != NULL, "missing bool");
TEST_END();
}
static void test_json_array_builder(void) {
TEST_BEGIN("JSON builder: array of strings");
char buf[256];
ha_json_builder_t jb;
ha_json_builder_init(&jb, buf, sizeof(buf));
ha_json_builder_begin_object(&jb);
ha_json_builder_key(&jb, "caps");
ha_json_builder_begin_array(&jb);
ha_json_builder_add_string(&jb, "camera");
ha_json_builder_add_string(&jb, "screen");
ha_json_builder_end_array(&jb);
ha_json_builder_end_object(&jb);
const char *r = ha_json_builder_str(&jb);
TEST_ASSERT(r != NULL, "builder returned NULL");
TEST_ASSERT(strstr(r, "\"camera\"") != NULL, "missing camera");
TEST_ASSERT(strstr(r, "\"screen\"") != NULL, "missing screen");
TEST_END();
}
/* ---------- 2. 工具函数 ---------- */
static void test_parse_homeagent(void) {
TEST_BEGIN("ha_cmd_parse_homeagent: basic");
const char *cap, *args;
ha_cmd_parse_homeagent("camerasue 5", &cap, &args);
TEST_ASSERT(strcmp(cap, "camerasue") == 0, "cap mismatch");
TEST_ASSERT(strcmp(args, "5") == 0, "args mismatch");
ha_cmd_parse_homeagent("screensee", &cap, &args);
TEST_ASSERT(strcmp(cap, "screensee") == 0, "cap mismatch (no args)");
TEST_ASSERT(strcmp(args, "") == 0, "args should be empty");
ha_cmd_parse_homeagent("homeagent-camerasue 3", &cap, &args);
TEST_ASSERT(strcmp(cap, "camerasue") == 0, "prefix not stripped");
TEST_ASSERT(strcmp(args, "3") == 0, "args after prefix");
ha_cmd_parse_homeagent("", &cap, &args);
TEST_ASSERT(strcmp(cap, "") == 0, "empty input");
TEST_END();
}
static void test_parse_json(void) {
TEST_BEGIN("ha_cmd_parse_json: action + json");
const char *action, *json_str;
ha_cmd_parse_json("computeruse {\"action\":\"click\",\"x\":100}",
&action, &json_str);
TEST_ASSERT(strcmp(action, "computeruse") == 0, "action mismatch");
TEST_ASSERT(strstr(json_str, "\"action\"") != NULL, "json missing");
ha_cmd_parse_json("screensee", &action, &json_str);
TEST_ASSERT(strcmp(action, "screensee") == 0, "action no json");
TEST_ASSERT(strcmp(json_str, "") == 0, "json should be empty");
TEST_END();
}
static void test_base64(void) {
TEST_BEGIN("ha_base64_encode: basic");
const uint8_t data[] = "Hello, World!";
char out[64];
int n = ha_base64_encode(data, 13, out, sizeof(out));
TEST_ASSERT(n > 0, "encode returned 0");
TEST_ASSERT(strcmp(out, "SGVsbG8sIFdvcmxkIQ==") == 0, "base64 mismatch");
const uint8_t jpeg[] = {0xFF, 0xD8, 0xFF};
n = ha_base64_encode(jpeg, 3, out, sizeof(out));
TEST_ASSERT(n > 0, "short encode failed");
TEST_ASSERT(strcmp(out, "/9j/") == 0, "jpeg magic mismatch");
/* 缓冲区不足 */
n = ha_base64_encode(data, 13, out, 5);
TEST_ASSERT(n > 5, "should return needed size");
TEST_END();
}
/* ---------- 3. WS 帧编解码 ---------- */
static void test_ws_frame(void) {
#if defined(_WIN32) || defined(_WIN64)
printf(" TEST: WS frame: encode/decode with mask ... SKIP: socketpair not available on Windows\n");
tests_skipped++;
return;
#else
TEST_BEGIN("WS frame: encode/decode with mask");
/* 使用 TCP 本地连接测试 */
int sv[2];
TEST_ASSERT(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0,
"socketpair failed");
const char *test_payload = "hello world";
int test_len = (int)strlen(test_payload);
/* 发送端:手动构造 WS 帧 */
uint8_t frame[1024];
int off = 0;
frame[off++] = 0x80 | 0x1;
frame[off++] = 0x80 | (uint8_t)test_len;
uint8_t mask[4] = {0x01, 0x02, 0x03, 0x04};
memcpy(frame + off, mask, 4); off += 4;
for (int i = 0; i < test_len; i++)
frame[off++] = test_payload[i] ^ mask[i & 3];
send(sv[0], (const char *)frame, off, 0);
/* 接收端 */
ha_transport_t transport = {
.send = transport_send, .recv = transport_recv,
.close = transport_close,
};
struct transport_ctx tctx_reader = { .sock = sv[1] };
transport.ctx = &tctx_reader;
ha_ws_t ws;
memset(&ws, 0, sizeof(ws));
ws.transport = &transport;
ws.connected = 1;
const uint8_t *payload;
int len;
int opcode = ha_ws_read_frame(&ws, &payload, &len);
TEST_ASSERT(opcode == 0x1, "expected text frame");
TEST_ASSERT(len == test_len, "length mismatch");
TEST_ASSERT(memcmp(payload, test_payload, len) == 0, "payload mismatch");
sock_close(sv[0]);
sock_close(sv[1]);
TEST_END();
#endif
}
/* ---------- 4. ha_version ---------- */
static void test_version(void) {
TEST_BEGIN("ha_version: returns non-empty");
const char *v = ha_version();
TEST_ASSERT(v != NULL && v[0] != '\0', "version empty");
printf("(v=%s) ", v);
TEST_END();
}
/* ---------- 5. 完整客户端生命周期 ---------- */
static volatile int lifecycle_state = 0;
static void test_lifecycle_on_state(int connected, void *userdata) {
(void)userdata;
lifecycle_state = connected ? 1 : 0;
}
static ha_status_t test_handler_camerasue(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf(" [handler] camerasue called, args=%s\n", args ? args : "");
result->status = 0;
result->output = "data:image/jpeg;base64,test123";
return HA_OK;
}
static ha_status_t test_handler_shell(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf(" [handler] shell called: %s\n", args ? args : "");
result->status = 0;
result->output = "shell output here";
return HA_OK;
}
static void test_client_lifecycle(void) {
TEST_BEGIN("Client lifecycle: connect -> hello -> bind -> ready");
mock_server_t *ms = mock_server_start(19890);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"camera", "status", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "shell", .handler = test_handler_shell},
{.command = "camerasue", .handler = test_handler_camerasue},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19890",
.token = "test-token",
.device = {
.device_id = "test-dev",
.name = "Test Device",
.kind = "camera",
.caps = caps,
},
.handlers = handlers,
.on_state = test_lifecycle_on_state,
.ping_interval = 30,
};
lifecycle_state = 0;
ha_client_t *client = ha_client_new(&config);
TEST_ASSERT(client != NULL, "client new failed");
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
TEST_ASSERT(lifecycle_state == 1, "should be connected");
/* 发送命令验证分发 */
mock_send_cmd(ms, "homeagent", "req-1", "camerasue 3");
usleep(200000);
ha_client_process(client);
mock_send_cmd(ms, "shell", "req-2", "ls -la");
usleep(200000);
ha_client_process(client);
/* 未注册的命令 */
mock_send_cmd(ms, "homeagent", "req-3", "unknowncmd");
usleep(100000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 6. 命令分发 ---------- */
static volatile int cmd_camerasue_fired = 0;
static volatile int cmd_shell_fired = 0;
static char cmd_req_id[128] = "";
static ha_status_t test_handler_camerasue2(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)userdata;
cmd_camerasue_fired = 1;
strncpy(cmd_req_id, req_id, sizeof(cmd_req_id) - 1);
if (strcmp(args, "5") != 0) {
printf(" [handler] args mismatch: expected '5', got '%s'\n", args);
result->status = 1;
result->error = "args mismatch";
return HA_OK;
}
result->status = 0;
result->output = "snapshot taken";
return HA_OK;
}
static ha_status_t test_handler_shell2(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)userdata;
cmd_shell_fired = 1;
if (strstr(args, "ls") == NULL) {
printf(" [handler] args should contain 'ls', got '%s'\n", args);
result->status = 1;
result->error = "args mismatch";
return HA_OK;
}
result->status = 0;
result->output = "file1.txt\nfile2.txt";
return HA_OK;
}
static void test_command_dispatch(void) {
TEST_BEGIN("Command dispatch: homeagent + shell");
mock_server_t *ms = mock_server_start(19891);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"camera", "cmd", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "shell", .handler = test_handler_shell2},
{.command = "camerasue", .handler = test_handler_camerasue2},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19891",
.token = "test-token",
.device = {
.device_id = "cmd-dev",
.name = "Cmd Test",
.kind = "camera",
.caps = caps,
},
.handlers = handlers,
.ping_interval = 30,
};
cmd_camerasue_fired = 0;
cmd_shell_fired = 0;
memset(cmd_req_id, 0, sizeof(cmd_req_id));
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 发送 homeagent 命令 */
mock_send_cmd(ms, "homeagent", "req-cam", "camerasue 5");
usleep(200000);
ha_client_process(client);
TEST_ASSERT(cmd_camerasue_fired == 1, "camerasue handler not called");
TEST_ASSERT(strcmp(cmd_req_id, "req-cam") == 0, "req_id mismatch");
/* 发送 shell 命令 */
mock_send_cmd(ms, "shell", "req-sh", "ls -la /tmp");
usleep(200000);
ha_client_process(client);
TEST_ASSERT(cmd_shell_fired == 1, "shell handler not called");
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 7. 事件上报 ---------- */
static void test_event_report(void) {
TEST_BEGIN("Event report: client sends event to server");
mock_server_t *ms = mock_server_start(19892);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19892",
.token = "test-token",
.device = {
.device_id = "evt-dev",
.name = "Event Test",
.kind = "camera",
.caps = (const char *[]){"camera", NULL},
},
.handlers = NULL,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
ha_client_send_event(client, "motion_detected", "{\"zone\":\"front_door\"}");
usleep(100000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 8. 状态上报 ---------- */
static void test_status_report(void) {
TEST_BEGIN("Status report: client sends status to server");
mock_server_t *ms = mock_server_start(19893);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19893",
.token = "test-token",
.device = {
.device_id = "st-dev",
.name = "Status Test",
.kind = "camera",
.caps = (const char *[]){"camera", NULL},
},
.handlers = NULL,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
ha_client_send_status(client, "offline");
usleep(100000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 9. 二进制分块回传 ---------- */
static void test_binary_chunked(void) {
TEST_BEGIN("Binary chunked transfer: send video data to server");
mock_server_t *ms = mock_server_start(19894);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_cmd_handler_def_t handlers[] = {
{.command = "camerasue", .handler = test_handler_camerasue},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19894",
.token = "test-token",
.device = {
.device_id = "bin-dev",
.name = "Binary Test",
.kind = "camera",
.caps = (const char *[]){"camera", NULL},
},
.handlers = handlers,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 模拟录像数据 */
uint8_t video_data[20000];
for (int i = 0; i < 20000; i++) video_data[i] = (uint8_t)(i % 251);
ha_client_send_data_chunked(client, "req-video", "camera_video",
"video/mp4", video_data, 20000);
usleep(500000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 10. 未注册命令处理 ---------- */
static void test_unsupported_command(void) {
TEST_BEGIN("Unsupported command: returns error gracefully");
mock_server_t *ms = mock_server_start(19895);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_cmd_handler_def_t handlers[] = {
{.command = "camerasue", .handler = test_handler_camerasue},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19895",
.token = "test-token",
.device = {
.device_id = "unsup-dev",
.name = "Unsupported Test",
.kind = "camera",
.caps = (const char *[]){"camera", NULL},
},
.handlers = handlers,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 发送未注册命令 */
mock_send_cmd(ms, "homeagent", "req-unsup", "screensee");
usleep(100000);
ha_client_process(client);
/* 发送已注册命令 */
mock_send_cmd(ms, "homeagent", "req-ok", "camerasue");
usleep(100000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 11. 语音数据接收 ---------- */
static volatile int speech_received = 0;
static uint8_t speech_data[4096];
static int speech_len = 0;
static void test_on_binary(const char *req_id, const char *kind,
const char *mime, const uint8_t *data,
int len, void *userdata) {
(void)req_id; (void)kind; (void)mime; (void)userdata;
speech_received = 1;
speech_len = len < (int)sizeof(speech_data) ? len : (int)sizeof(speech_data);
memcpy(speech_data, data, (size_t)speech_len);
}
static void test_speech_receive(void) {
TEST_BEGIN("Speech data receive: cmd_speech_start -> binary -> end");
mock_server_t *ms = mock_server_start(19896);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19896",
.token = "test-token",
.device = {
.device_id = "speech-dev",
.name = "Speech Test",
.kind = "speaker",
.caps = (const char *[]){"speaker", NULL},
},
.handlers = NULL,
.on_binary = test_on_binary,
.ping_interval = 30,
};
speech_received = 0;
speech_len = 0;
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* mock 服务器发送语音数据 */
const char *audio_data = "RIFF....fake-wav-data....";
mock_send_text(ms->client_fd,
"{\"op\":\"cmd_speech_start\",\"req_id\":\"req-speech\","
"\"kind\":\"speech\",\"mime\":\"audio/wav\",\"total\":25}");
usleep(50000);
mock_send_binary(ms->client_fd, (const uint8_t *)audio_data, 25);
usleep(50000);
mock_send_text(ms->client_fd,
"{\"op\":\"cmd_speech_end\",\"req_id\":\"req-speech\"}");
usleep(200000);
/* 处理帧 */
for (int i = 0; i < 50; i++) {
ha_client_process(client);
if (speech_received) break;
usleep(20000);
}
TEST_ASSERT(speech_received == 1, "speech handler not called");
TEST_ASSERT(speech_len == 25, "speech length mismatch");
TEST_ASSERT(memcmp(speech_data, audio_data, 25) == 0, "speech data mismatch");
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 12. 二进制分块 via handler ---------- */
static ha_status_t test_handler_video(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf(" [handler] video record %s\n", args ? args : "");
static uint8_t video[5000];
for (int i = 0; i < 5000; i++) video[i] = (uint8_t)(i & 0xFF);
result->status = 0;
result->has_binary = 1;
result->binary_data = video;
result->binary_len = 5000;
result->binary_mime = "video/mp4";
return HA_OK;
}
static void test_binary_via_handler(void) {
TEST_BEGIN("Binary via handler: handler sets has_binary, SDK auto-chunks");
mock_server_t *ms = mock_server_start(19897);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
ha_cmd_handler_def_t handlers[] = {
{.command = "camerasue", .handler = test_handler_video},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19897",
.token = "test-token",
.device = {
.device_id = "bin2-dev",
.name = "Binary Via Handler",
.kind = "camera",
.caps = (const char *[]){"camera", NULL},
},
.handlers = handlers,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
mock_send_cmd(ms, "homeagent", "req-video2", "camerasue 10");
usleep(500000);
ha_client_process(client);
usleep(100000);
ha_client_process(client);
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 13. computeruse结构化 JSON 命令分发 ---------- */
static volatile int computeruse_fired = 0;
static char computeruse_action[64] = "";
static int computeruse_x = 0;
static int computeruse_y = 0;
static ha_status_t test_handler_computeruse(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
computeruse_fired = 1;
/* 解析 JSON 参数 */
if (args && args[0] == '{') {
ha_json_node_t *root = ha_json_parse(args);
if (root) {
const char *act = ha_json_get_string(root, "action");
if (act) strncpy(computeruse_action, act, sizeof(computeruse_action) - 1);
computeruse_x = ha_json_get_int(root, "x", 0);
computeruse_y = ha_json_get_int(root, "y", 0);
ha_json_free(root);
}
}
result->status = 0;
result->output = "clicked at (100,200)";
return HA_OK;
}
static void test_computeruse_dispatch(void) {
TEST_BEGIN("Computeruse: structured JSON command dispatch");
mock_server_t *ms = mock_server_start(19898);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"computeruse", "cmd", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "computeruse", .handler = test_handler_computeruse},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19898",
.token = "test-token",
.device = {
.device_id = "cu-dev",
.name = "ComputerUse Test",
.kind = "computer",
.caps = caps,
},
.handlers = handlers,
.ping_interval = 30,
};
computeruse_fired = 0;
memset(computeruse_action, 0, sizeof(computeruse_action));
computeruse_x = 0;
computeruse_y = 0;
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 发送 computeruse 命令(带结构化 JSON 参数) */
mock_send_cmd(ms, "homeagent", "req-cu",
"computeruse {\"action\":\"click\",\"x\":100,\"y\":200}");
usleep(200000);
ha_client_process(client);
TEST_ASSERT(computeruse_fired == 1, "computeruse handler not called");
TEST_ASSERT(strcmp(computeruse_action, "click") == 0,
"action should be 'click'");
TEST_ASSERT(computeruse_x == 100, "x should be 100");
TEST_ASSERT(computeruse_y == 200, "y should be 200");
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 14. clipboardsee/clipboardsue 命令分发 ---------- */
static volatile int clipboardsee_fired = 0;
static volatile int clipboardsue_fired = 0;
static char clipboardsue_text[256] = "";
static ha_status_t test_handler_clipboardsee(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)args; (void)userdata;
clipboardsee_fired = 1;
result->status = 0;
result->output = "clipboard content here";
return HA_OK;
}
static ha_status_t test_handler_clipboardsue(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
clipboardsue_fired = 1;
if (args) strncpy(clipboardsue_text, args, sizeof(clipboardsue_text) - 1);
result->status = 0;
result->output = "clipboard set";
return HA_OK;
}
static void test_clipboard_dispatch(void) {
TEST_BEGIN("Clipboard: clipboardsee + clipboardsue dispatch");
mock_server_t *ms = mock_server_start(19899);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"clipboard", "cmd", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "clipboardsee", .handler = test_handler_clipboardsee},
{.command = "clipboardsue", .handler = test_handler_clipboardsue},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19899",
.token = "test-token",
.device = {
.device_id = "clip-dev",
.name = "Clipboard Test",
.kind = "computer",
.caps = caps,
},
.handlers = handlers,
.ping_interval = 30,
};
clipboardsee_fired = 0;
clipboardsue_fired = 0;
memset(clipboardsue_text, 0, sizeof(clipboardsue_text));
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 发送 clipboardsee 命令 */
mock_send_cmd(ms, "homeagent", "req-cs", "clipboardsee");
usleep(200000);
ha_client_process(client);
TEST_ASSERT(clipboardsee_fired == 1, "clipboardsee handler not called");
/* 发送 clipboardsue 命令 */
mock_send_cmd(ms, "homeagent", "req-cw", "clipboardsue Hello World");
usleep(200000);
ha_client_process(client);
TEST_ASSERT(clipboardsue_fired == 1, "clipboardsue handler not called");
TEST_ASSERT(strcmp(clipboardsue_text, "Hello World") == 0,
"clipboardsue text mismatch");
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 15. screensee带 data URL 的结果回传 ---------- */
static ha_status_t test_handler_screensee(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)args; (void)userdata;
result->status = 0;
/* 返回 data URL 格式的 base64 图像数据 */
result->output = "data:image/jpeg;base64,/9j/4AAQSkZJRg==";
return HA_OK;
}
static void test_screensee_data_url(void) {
TEST_BEGIN("Screensee: handler returns data URL result");
mock_server_t *ms = mock_server_start(19900);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"screen", "cmd", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "screensee", .handler = test_handler_screensee},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19900",
.token = "test-token",
.device = {
.device_id = "see-dev",
.name = "Screensee Test",
.kind = "computer",
.caps = caps,
},
.handlers = handlers,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 发送 screensee 命令 */
mock_send_cmd(ms, "homeagent", "req-see", "screensee");
usleep(200000);
ha_client_process(client);
/* handler 已被调用,且返回了 data URL无法直接验证回执内容但 handler 已执行) */
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ---------- 16. info_json 设备信息 ---------- */
static void test_info_json(void) {
TEST_BEGIN("Device info_json: included in hello message");
mock_server_t *ms = mock_server_start(19901);
TEST_ASSERT(ms != NULL, "mock server start failed");
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
const char *caps[] = {"camera", NULL};
ha_cmd_handler_def_t handlers[] = {
{.command = "camerasue", .handler = test_handler_camerasue},
{.command = NULL},
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:19901",
.token = "test-token",
.device = {
.device_id = "info-dev",
.name = "Info Test",
.kind = "camera",
.caps = caps,
.info_json = "{\"chip\":\"ESP32-S3\",\"psram\":8}",
},
.handlers = handlers,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
ha_status_t st = ha_client_start(client);
TEST_ASSERT(st == HA_OK, "client start failed");
usleep(200000);
/* 连接成功info_json 已通过 hello 消息发送 */
ha_client_stop(client);
ha_client_destroy(client);
mock_server_stop(ms);
TEST_END();
}
/* ==================================================================
* 主函数
* ================================================================== */
int main(void) {
#if !defined(_WIN32) && !defined(_WIN64)
signal(SIGPIPE, SIG_IGN);
#endif
printf("========================================\n");
printf(" ha_remotedevice 全面测试\n");
printf("========================================\n\n");
/* ---- JSON 解析/构建 ---- */
printf("[JSON]\n");
test_json_parser();
test_json_int();
test_json_builder();
test_json_array_builder();
/* ---- 工具函数 ---- */
printf("\n[Utilities]\n");
test_parse_homeagent();
test_parse_json();
test_base64();
/* ---- WS 协议 ---- */
printf("\n[WebSocket]\n");
test_ws_frame();
/* ---- SDK 核心 ---- */
printf("\n[SDK Core]\n");
test_version();
test_client_lifecycle();
test_command_dispatch();
/* ---- 协议功能 ---- */
printf("\n[Protocol]\n");
test_event_report();
test_status_report();
test_binary_chunked();
test_unsupported_command();
test_speech_receive();
test_binary_via_handler();
/* ---- 扩展命令 ---- */
printf("\n[Extended Commands]\n");
test_computeruse_dispatch();
test_clipboard_dispatch();
test_screensee_data_url();
test_info_json();
/* ---- 汇总 ---- */
printf("\n========================================\n");
printf(" 结果: %d passed, %d failed, %d skipped\n",
tests_passed, tests_failed, tests_skipped);
printf("========================================\n");
return tests_failed > 0 ? 1 : 0;
}