diff --git a/tests/Makefile.am b/tests/Makefile.am index 093b5940..111b080c 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -133,6 +133,7 @@ check_PROGRAMS = \ if OS_LINUX check_PROGRAMS += test_auto_socket_dynamic test_standby test_standby_transport test_group_connect_standby test_android_udp_log test_pool_corruption_log check_PROGRAMS += test_reality_fallback test_stcp_reality_rotation test_uasync_regressions test_ll_queue_regressions test_etcp_padding +check_PROGRAMS += test_android_replies endif # Silero VAD: только при включённом ONNX Runtime (--with-silero-vad) @@ -192,6 +193,16 @@ test_group_connect_standby_LDADD = $(LIBUTUN) $(COMMON_LIBS) test_android_udp_log_SOURCES = test_android_udp_log.c ../tools/chatgui-android/jni_bridge/android_udp_log.c test_android_udp_log_LDADD = $(COMMON_LIBS) test_android_udp_log_LDFLAGS = -Wl,--wrap=uasync_set_timeout -Wl,--wrap=uasync_cancel_timeout +test_android_replies_SOURCES = test_android_replies.c \ + ../tools/chatgui-android/jni_bridge/android_jni_bridge.c ../tools/chatgui-android/jni_bridge/android_udp_log.c \ + ../tools/chatgui-android/libutun_lite/instance_lite.c ../tools/chatgui-android/libutun_lite/utun_config_api.c \ + ../tools/chatgui-android/libutun_lite/attachment_sender.c ../tools/chatgui-android/libutun_lite/photo_sender.c \ + ../tools/chatgui-android/libutun_lite/video_sender.c ../tools/chatgui-android/libutun_lite/voice_recorder.c \ + ../tools/chatgui-android/libutun_lite/standby.c +test_android_replies_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src/chat -I$(top_srcdir)/src/media_async +test_android_replies_CFLAGS = $(AM_CFLAGS) -ffunction-sections -fdata-sections +test_android_replies_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_android_replies_LDFLAGS = -Wl,--gc-sections -Wl,--wrap=instance_lite_get_instance -Wl,--wrap=instance_lite_get_uasync test_pool_corruption_log_SOURCES = test_pool_corruption_log.c test_pool_corruption_log_LDADD = $(COMMON_LIBS) diff --git a/tests/test_android_replies.c b/tests/test_android_replies.c new file mode 100644 index 00000000..0cc8ab52 --- /dev/null +++ b/tests/test_android_replies.c @@ -0,0 +1,128 @@ +/* Android bridge + настоящее ядро: ссылки, подтверждения, media workers и старая история. + * Подменяем только владельца instance, чтобы весь сценарий выполнялся в одном uasync-потоке. */ +#include +#include +#include +#include "../lib/mem.h" +#include "../lib/debug_config.h" +#include "../src/utun_instance.h" +#include "../src/chat/chat_core.h" +#include "../src/chat/chat_event.h" +#include "../tools/chatgui-android/jni_bridge/android_jni_bridge.h" +#include "test_utils.h" + +#define REQUIRE(value) do { if (!(value)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "Android reply check failed line=%d", __LINE__); return 1; } } while (0) +static struct UTUN_INSTANCE* instance; +static struct UASYNC* async; +static int results[32]; +struct UTUN_INSTANCE* __wrap_instance_lite_get_instance(void) { return instance; } +struct UASYNC* __wrap_instance_lite_get_uasync(void) { return async; } + +static void event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int length) { + (void)inst; + if (type != CHAT_EVT_MSG_SUBMITTED || length != 9) return; + uint64_t id = 0; + for (int i = 0; i < 8; i++) id |= (uint64_t)data[i] << (8 * i); + if (id < 32) results[id] = data[8] ? 1 : -1; +} + +static int wait_result(int id) { + for (int i = 0; i < 500 && !results[id]; i++) uasync_poll(async, 20); + return results[id]; +} + +static int extract(const char* json, const char* path, char* value, size_t capacity) { + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(instance->topo_sqlite_db, "SELECT json_extract(?1,?2)", -1, &st, NULL) != SQLITE_OK) return -1; + sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, path, -1, SQLITE_STATIC); + int rc = sqlite3_step(st); + if (rc == SQLITE_ROW) snprintf(value, capacity, "%s", sqlite3_column_text(st, 0)); + sqlite3_finalize(st); + return rc == SQLITE_ROW ? 0 : -1; +} + +static int check_reply(const char* channel, const char* original, const char* type) { + char* json = utun_bridge_get_messages_json(channel, 1, NULL, 0); + char reply[65], content_type[64]; + int ok = !extract(json, "$[0].replyTo", reply, sizeof(reply)) && !strcmp(reply, original) && + !extract(json, "$[0].contentType", content_type, sizeof(content_type)) && !strcmp(content_type, type); + u_free(json); return ok; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); + char directory[512] = "/tmp/utun_android_replies_XXXXXX", config[1024], channel[64], hex[65]; + REQUIRE(!test_mkdtemp(directory)); + snprintf(config, sizeof(config), "[global]\ndb_path=%s\n[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory); + async = uasync_create(); REQUIRE(async); + instance = utun_instance_create_from_str(async, config); REQUIRE(instance); + REQUIRE(!utun_core_start(instance) && !chat_service_start(instance)); + uasync_mark_running(async); + chat_event_set_handler(instance, event); + chat_core_create_channel_auto(instance, "Android replies"); + char* json = utun_bridge_get_channels_json(); + REQUIRE(!extract(json, "$[0].id", channel, sizeof(channel))); u_free(json); + uint8_t empty[32] = {0}, original[32], missing[32]; memset(missing, 0xff, sizeof(missing)); + REQUIRE(!utun_bridge_send_message(channel, "Привет 😀 \"original\"\nnext line", empty, 1) && wait_result(1) == 1); + json = utun_bridge_get_messages_json(channel, 1, NULL, 0); + REQUIRE(!extract(json, "$[0].messageId", hex, sizeof(hex)) && strlen(hex) == 64); + REQUIRE(strstr(json, "Привет 😀")); u_free(json); + for (unsigned i = 0; i < 32; i++) { unsigned value; REQUIRE(sscanf(hex + 2 * i, "%2x", &value) == 1); original[i] = value; } + + /* Оригинал выходит из последнего окна; одинаковые timestamp сортируются по подписи. */ + for (int i = 0; i < 220; i++) { + char body[32]; snprintf(body, sizeof(body), "History %d", i); + struct chat_msg_submit req = {0}; req.inst = instance; + snprintf(req.channel_id, sizeof(req.channel_id), "%s", channel); strcpy(req.content_type, "text/plain"); + req.data = (uint8_t*)body; req.data_len = strlen(body); + REQUIRE(!chat_core_submit_message(instance, &req)); + } + json = utun_bridge_get_messages_json(channel, 100, NULL, 0); REQUIRE(!strstr(json, hex)); u_free(json); + json = utun_bridge_get_messages_json(channel, 100, original, 0); REQUIRE(strstr(json, hex)); + char length[32]; + sqlite3_stmt* st = NULL; + REQUIRE(sqlite3_prepare_v2(instance->topo_sqlite_db, "SELECT json_array_length(?)", -1, &st, NULL) == SQLITE_OK); + sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); REQUIRE(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_int(st, 0) == 100); + sqlite3_finalize(st); u_free(json); + json = utun_bridge_get_messages_json(channel, 1, original, 1); + REQUIRE(strstr(json, "Привет 😀") && !extract(json, "$[0].replyTo", length, sizeof(length)) && !length[0]); u_free(json); + json = utun_bridge_get_messages_json(channel, 1, missing, 1); REQUIRE(!strcmp(json, "[]")); u_free(json); + REQUIRE(!utun_bridge_send_message(channel, "Reply to old message", original, 2) && wait_result(2) == 1); + REQUIRE(check_reply(channel, hex, "text/plain")); + REQUIRE(!utun_bridge_send_message(channel, "Missing original", missing, 3) && wait_result(3) == -1); + REQUIRE(check_reply(channel, hex, "text/plain")); + + char file[1024]; snprintf(file, sizeof(file), "%s/source.bin", directory); + FILE* source = fopen(file, "wb"); REQUIRE(source); + REQUIRE(fwrite("Android attachment", 1, 18, source) == 18 && fclose(source) == 0); + REQUIRE(!utun_bridge_attachment_send(channel, file, directory, original, 4) && wait_result(4) == 1); + REQUIRE(check_reply(channel, hex, "application/octet-stream")); + REQUIRE(!utun_bridge_photo_send(channel, file, directory, 24, 24, original, 5) && wait_result(5) == 1); + REQUIRE(check_reply(channel, hex, "image/jpeg")); + /* Мост принимает уже подготовленный MP4: содержимое не декодируется повторно. */ + REQUIRE(!utun_bridge_video_send(channel, file, directory, 1000, 24, 24, original, 6) && wait_result(6) == 1); + REQUIRE(check_reply(channel, hex, "video/mp4")); + REQUIRE(access(file, F_OK) != 0); + + REQUIRE(!utun_bridge_voice_start(channel, original, 7)); + int16_t pcm[960] = {0}; + for (int i = 0; i < 30; i++) { REQUIRE(!utun_bridge_voice_feed(pcm, 960)); uasync_poll(async, 20); } + /* Poll может вернуться немедленно по событию; длительность измеряется CLOCK_MONOTONIC. */ + struct timespec pause = {0, 650000000}; nanosleep(&pause, NULL); + int duration = 0; + REQUIRE(!utun_bridge_voice_stop(&duration) && duration >= 500 && wait_result(7) == 1); + REQUIRE(check_reply(channel, hex, "audio/opus")); + REQUIRE(!utun_bridge_voice_start(channel, original, 8)); utun_bridge_voice_cancel(); + uasync_poll(async, 0); REQUIRE(results[8] == 0); + REQUIRE(utun_bridge_voice_stop(&duration) < 0); + REQUIRE(!utun_bridge_attachment_send(channel, "/nonexistent/android-reply", directory, original, 9) && wait_result(9) == -1); + REQUIRE(check_reply(channel, hex, "audio/opus")); + + /* Ссылка остаётся в JSON после перезапуска сервиса. */ + chat_service_stop(instance); REQUIRE(!chat_service_start(instance)); + REQUIRE(check_reply(channel, hex, "audio/opus")); + utun_bridge_destroy(); + utun_instance_destroy(instance); uasync_destroy(async, 0); + printf("test_android_replies: text, media, voice, results, history and restart PASS\n"); + return 0; +}