// SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of CProof. // See LICENSE for the full GPLv3 text and the special OpenSSL linking exception. /* * test_database_stress.c * * Stress tests for flat-file database I/O: rapid-fire writes, MUC messages * with many participants, large message bodies, index/cache correctness, * export/import merge under load, and deep LMC correction chains. */ #include "prof_cmocka.h" #include #include #include #include #include #include #include #include "config.h" #include "database_flatfile.h" /* ================================================================ * Helpers * ================================================================ */ /* Create a temporary directory under /tmp and return its path. * Caller must g_free(). */ static char* _make_tmpdir(void) { char tmpl[] = "/tmp/profstress_XXXXXX"; char* dir = mkdtemp(tmpl); assert_non_null(dir); return g_strdup(dir); } /* Write N lines to a file and return the path. * Each line gets a unique timestamp, sender from the pool, and body. * Caller must g_free() the path. */ static char* _write_n_lines(const char* dir, int n, const char* type, const char** senders, int n_senders, const char* body_prefix, int body_size) { char* path = g_strdup_printf("%s/history.log", dir); FILE* fp = fopen(path, "w"); assert_non_null(fp); fprintf(fp, "%s", FLATFILE_HEADER); /* Generate body of requested size */ char* body = NULL; if (body_size > 0) { body = g_malloc(body_size + 1); int prefix_len = body_prefix ? (int)strlen(body_prefix) : 0; if (prefix_len > 0 && prefix_len < body_size) { memcpy(body, body_prefix, prefix_len); } else { prefix_len = 0; } /* Fill remaining with printable chars */ for (int i = prefix_len; i < body_size; i++) { body[i] = 'A' + (i % 26); } body[body_size] = '\0'; } GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < n; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); const char* sender = senders[i % n_senders]; char* sid = g_strdup_printf("stress-sid-%d", i); char* aid = g_strdup_printf("stress-aid-%d", i); const char* msg = body ? body : body_prefix; if (!msg) { msg = g_strdup_printf("Stress message #%d from %s", i, sender); ff_write_line(fp, ts_str, type, "none", sid, aid, NULL, sender, "resource", NULL, NULL, -1, msg); g_free((char*)msg); } else { ff_write_line(fp, ts_str, type, "none", sid, aid, NULL, sender, "resource", NULL, NULL, -1, msg); } g_free(sid); g_free(aid); } g_date_time_unref(base); g_free(body); fclose(fp); return path; } /* Remove directory and its contents */ static void _cleanup_dir(const char* dir) { auto_gchar gchar* cmd = g_strdup_printf("rm -rf '%s'", dir); int ret = system(cmd); (void)ret; } /* Current time in milliseconds */ static long long _now_ms(void) { struct timeval tv; gettimeofday(&tv, NULL); return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000; } /* ================================================================ * High-throughput write + parse * ================================================================ */ void test_stress_rapid_write_parse_1000(void** state) { char tmppath[] = "/tmp/profstress_rw_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 3, 1, 0, 0, 0); long long t0 = _now_ms(); for (int i = 0; i < 1000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32]; snprintf(sid, sizeof(sid), "sid-%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, NULL, NULL, "alice@example.com", "phone", "bob@example.com", NULL, -1, "Quick test message number N"); } fclose(fp); long long t_write = _now_ms() - t0; /* Now parse all lines back */ fp = fopen(tmppath, "r"); assert_non_null(fp); int parsed = 0; t0 = _now_ms(); while (1) { gboolean truncated = FALSE; char* buf = ff_readline(fp, &truncated); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; ff_parsed_line_free(pl); } } fclose(fp); unlink(tmppath); long long t_parse = _now_ms() - t0; g_date_time_unref(base); assert_int_equal(parsed, 1000); /* Sanity: 1000 writes/parses shouldn't take more than 5 seconds */ assert_true(t_write < 5000); assert_true(t_parse < 5000); } void test_stress_rapid_write_parse_10000(void** state) { char tmppath[] = "/tmp/profstress_rw10k_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 3, 1, 0, 0, 0); for (int i = 0; i < 10000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32], aid[32]; snprintf(sid, sizeof(sid), "sid10k-%d", i); snprintf(aid, sizeof(aid), "aid10k-%d", i); ff_write_line(fp, ts_str, "chat", "omemo", sid, aid, NULL, "user@jabber.org", "laptop", "peer@jabber.org", "mobile", -1, "Stress test message with metadata"); } fclose(fp); g_date_time_unref(base); /* Parse all back */ fp = fopen(tmppath, "r"); assert_non_null(fp); int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; ff_parsed_line_free(pl); } } fclose(fp); unlink(tmppath); assert_int_equal(parsed, 10000); } /* ================================================================ * MUC (chat room) messages * ================================================================ */ void test_stress_muc_many_participants(void** state) { /* 50 participants, 20 messages each = 1000 messages */ const int n_participants = 50; const int msgs_per_participant = 20; const int total = n_participants * msgs_per_participant; char tmppath[] = "/tmp/profstress_muc_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 6, 15, 14, 0, 0); for (int i = 0; i < total; i++) { int participant_idx = i % n_participants; GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char from_jid[128]; snprintf(from_jid, sizeof(from_jid), "room@conference.example.com"); char from_resource[64]; snprintf(from_resource, sizeof(from_resource), "participant_%d", participant_idx); char sid[48]; snprintf(sid, sizeof(sid), "muc-sid-%d", i); char body[128]; snprintf(body, sizeof(body), "Message from participant %d, iteration %d", participant_idx, i / n_participants); ff_write_line(fp, ts_str, "muc", "none", sid, NULL, NULL, from_jid, from_resource, NULL, NULL, -1, body); } fclose(fp); g_date_time_unref(base); /* Parse and verify all MUC messages */ fp = fopen(tmppath, "r"); assert_non_null(fp); int parsed = 0; int muc_count = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; if (g_strcmp0(pl->type, "muc") == 0) muc_count++; /* Verify resource is participant_N */ assert_non_null(pl->from_resource); assert_true(g_str_has_prefix(pl->from_resource, "participant_")); ff_parsed_line_free(pl); } } fclose(fp); unlink(tmppath); assert_int_equal(parsed, total); assert_int_equal(muc_count, total); } void test_stress_muc_rapid_messages(void** state) { /* 5000 messages in a MUC from 10 users, 1 per second */ const int n_users = 10; const int total = 5000; char tmppath[] = "/tmp/profstress_mucrapid_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < total; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char from_res[32]; snprintf(from_res, sizeof(from_res), "user%d", i % n_users); char sid[48]; snprintf(sid, sizeof(sid), "rapid-muc-%d", i); ff_write_line(fp, ts_str, "muc", "omemo", sid, sid, NULL, "devroom@muc.jabber.org", from_res, NULL, NULL, -1, "rapid fire MUC message"); } fclose(fp); g_date_time_unref(base); /* Parse back */ fp = fopen(tmppath, "r"); assert_non_null(fp); int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] != '#' && buf[0] != '\0') { ff_parsed_line_t* pl = ff_parse_line(buf); if (pl) { parsed++; ff_parsed_line_free(pl); } } free(buf); } fclose(fp); unlink(tmppath); assert_int_equal(parsed, total); } /* ================================================================ * Large messages * ================================================================ */ void test_stress_large_message_1mb(void** state) { /* Generate a 1 MB message body */ const int body_size = 1024 * 1024; char* body = g_malloc(body_size + 1); for (int i = 0; i < body_size; i++) { body[i] = 'A' + (i % 26); } body[body_size] = '\0'; char tmppath[] = "/tmp/profstress_1mb_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); ff_write_line(fp, "2026-01-01T00:00:00+00:00", "chat", "none", "big-msg-1", NULL, NULL, "alice@example.com", "desktop", "bob@example.com", NULL, -1, body); fclose(fp); /* Parse back */ fp = fopen(tmppath, "r"); assert_non_null(fp); char* buf = ff_readline(fp, NULL); fclose(fp); unlink(tmppath); assert_non_null(buf); ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); assert_non_null(pl); assert_non_null(pl->message); assert_int_equal((int)strlen(pl->message), body_size); /* Spot-check content */ assert_int_equal(pl->message[0], 'A'); assert_int_equal(pl->message[25], 'Z'); assert_int_equal(pl->message[26], 'A'); assert_string_equal(pl->from_jid, "alice@example.com"); ff_parsed_line_free(pl); g_free(body); } void test_stress_large_message_body_with_special_chars(void** state) { /* 100 KB body with newlines, backslashes, pipes, brackets */ const int body_size = 100 * 1024; GString* gs = g_string_sized_new(body_size); for (int i = 0; i < body_size / 10; i++) { g_string_append(gs, "Line\\n|]\r\n"); } char tmppath[] = "/tmp/profstress_special_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); ff_write_line(fp, "2026-06-01T12:00:00+00:00", "chat", "pgp", "special-1", "special-aid-1", NULL, "eve@evil.org", "laptop", "target@good.org", NULL, -1, gs->str); fclose(fp); /* Parse back */ fp = fopen(tmppath, "r"); assert_non_null(fp); char* buf = ff_readline(fp, NULL); fclose(fp); unlink(tmppath); assert_non_null(buf); ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); assert_non_null(pl); assert_non_null(pl->message); /* The body should survive the escape→write→read→unescape roundtrip */ assert_string_equal(pl->message, gs->str); assert_string_equal(pl->enc, "pgp"); ff_parsed_line_free(pl); g_string_free(gs, TRUE); } void test_stress_many_large_messages_100(void** state) { /* 100 messages, each 50 KB */ const int n_msgs = 100; const int body_size = 50 * 1024; char* body = g_malloc(body_size + 1); for (int i = 0; i < body_size; i++) body[i] = '0' + (i % 10); body[body_size] = '\0'; char tmppath[] = "/tmp/profstress_100big_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < n_msgs; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i * 60); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32]; snprintf(sid, sizeof(sid), "big-%d", i); ff_write_line(fp, ts_str, "chat", "omemo", sid, NULL, NULL, "sender@example.com", "res", "recv@example.com", NULL, -1, body); } fclose(fp); g_date_time_unref(base); /* Parse all, verify body integrity */ fp = fopen(tmppath, "r"); assert_non_null(fp); int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { assert_int_equal((int)strlen(pl->message), body_size); parsed++; ff_parsed_line_free(pl); } } fclose(fp); unlink(tmppath); assert_int_equal(parsed, n_msgs); g_free(body); } /* ================================================================ * Index: write 10000 lines, parse them all back (pure I/O stress) * (ff_state_new/ensure_fresh require full database_flatfile.c linking; * those are covered by the functional tests instead.) * ================================================================ */ void test_stress_index_build_10000_lines(void** state) { auto_gchar gchar* dir = _make_tmpdir(); const char* senders[] = { "a@x.org", "b@x.org", "c@x.org" }; auto_gchar gchar* path = _write_n_lines(dir, 10000, "chat", senders, 3, "msg", 0); /* Parse all 10000 lines back */ FILE* fp = fopen(path, "r"); assert_non_null(fp); int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; ff_parsed_line_free(pl); } } fclose(fp); assert_int_equal(parsed, 10000); /* Verify dedup via manual hash-set scan */ fp = fopen(path, "r"); assert_non_null(fp); GHashTable* aid_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GHashTable* sid_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { if (pl->archive_id) g_hash_table_add(aid_set, g_strdup(pl->archive_id)); if (pl->stanza_id && pl->from_jid) g_hash_table_insert(sid_map, g_strdup(pl->stanza_id), g_strdup(pl->from_jid)); ff_parsed_line_free(pl); } } fclose(fp); assert_int_equal((int)g_hash_table_size(aid_set), 10000); assert_int_equal((int)g_hash_table_size(sid_map), 10000); assert_true(g_hash_table_contains(aid_set, "stress-aid-0")); assert_true(g_hash_table_contains(aid_set, "stress-aid-9999")); const char* s0 = g_hash_table_lookup(sid_map, "stress-sid-0"); assert_string_equal(s0, "a@x.org"); const char* s1 = g_hash_table_lookup(sid_map, "stress-sid-1"); assert_string_equal(s1, "b@x.org"); g_hash_table_destroy(aid_set); g_hash_table_destroy(sid_map); _cleanup_dir(dir); } void test_stress_index_extend_append(void** state) { /* Write 500 lines, then append 500 more, parse all 1000 */ auto_gchar gchar* dir = _make_tmpdir(); const char* senders[] = { "writer@test.org" }; auto_gchar gchar* path = _write_n_lines(dir, 500, "chat", senders, 1, "initial", 0); /* Append 500 more lines */ FILE* fp = fopen(path, "a"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 8, 20); for (int i = 500; i < 1000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32], aid[32]; snprintf(sid, sizeof(sid), "stress-sid-%d", i); snprintf(aid, sizeof(aid), "stress-aid-%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, aid, NULL, "writer@test.org", "resource", NULL, NULL, -1, "appended message"); } fclose(fp); g_date_time_unref(base); /* Parse all 1000 lines */ fp = fopen(path, "r"); assert_non_null(fp); int parsed = 0; GHashTable* aids = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; if (pl->archive_id) g_hash_table_add(aids, g_strdup(pl->archive_id)); ff_parsed_line_free(pl); } } fclose(fp); assert_int_equal(parsed, 1000); assert_int_equal((int)g_hash_table_size(aids), 1000); assert_true(g_hash_table_contains(aids, "stress-aid-999")); g_hash_table_destroy(aids); _cleanup_dir(dir); } void test_stress_id_cache_dedup_correctness(void** state) { /* Write 2000 lines where every 10th has a duplicate archive_id. * Verify dedup correctness by parsing and using a manual hash set. */ auto_gchar gchar* dir = _make_tmpdir(); char* path = g_strdup_printf("%s/history.log", dir); FILE* fp = fopen(path, "w"); assert_non_null(fp); fprintf(fp, "%s", FLATFILE_HEADER); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < 2000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32], aid[32]; snprintf(sid, sizeof(sid), "dedup-sid-%d", i); /* Every 10th line (starting from i=10) reuses archive_id "stress-aid-0" */ if (i > 0 && i % 10 == 0) { snprintf(aid, sizeof(aid), "stress-aid-0"); } else { snprintf(aid, sizeof(aid), "stress-aid-%d", i); } ff_write_line(fp, ts_str, "chat", "none", sid, aid, NULL, "sender@x.org", "res", NULL, NULL, -1, "dedup test"); } fclose(fp); g_date_time_unref(base); /* Parse and build dedup set */ fp = fopen(path, "r"); assert_non_null(fp); GHashTable* aid_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GHashTable* sid_set = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { parsed++; if (pl->archive_id) g_hash_table_add(aid_set, g_strdup(pl->archive_id)); if (pl->stanza_id && pl->from_jid) g_hash_table_insert(sid_set, g_strdup(pl->stanza_id), g_strdup(pl->from_jid)); ff_parsed_line_free(pl); } } fclose(fp); assert_int_equal(parsed, 2000); /* 2000 stanza IDs are all unique */ assert_int_equal((int)g_hash_table_size(sid_set), 2000); /* archive_ids: 199 are duplicates (i=10,20,...,1990 all map to stress-aid-0) */ /* unique aids = 2000 - 199 = 1801 */ assert_int_equal((int)g_hash_table_size(aid_set), 1801); assert_true(g_hash_table_contains(aid_set, "stress-aid-0")); assert_true(g_hash_table_contains(aid_set, "stress-aid-1999")); g_hash_table_destroy(aid_set); g_hash_table_destroy(sid_set); g_free(path); _cleanup_dir(dir); } /* ================================================================ * Export / import merge dedup * ================================================================ */ void test_stress_export_merge_dedup_1000(void** state) { /* Simulate merge: write 1000 lines, read them all, write again with * 500 new lines, verify dedup via hash set */ char tmppath[] = "/tmp/profstress_merge_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); fprintf(fp, "%s", FLATFILE_HEADER); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < 1000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[48]; snprintf(sid, sizeof(sid), "merge-sid-%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, NULL, NULL, "alice@x.org", "r", "bob@x.org", NULL, -1, "existing message"); } fclose(fp); /* Read all lines into a dedup set (keyed by stanza_id) */ fp = fopen(tmppath, "r"); assert_non_null(fp); GHashTable* seen = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL); GSList* existing = NULL; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) { if (pl->stanza_id) g_hash_table_add(seen, g_strdup(pl->stanza_id)); existing = g_slist_prepend(existing, pl); } } fclose(fp); existing = g_slist_reverse(existing); assert_int_equal((int)g_slist_length(existing), 1000); /* Now create 500 new + 500 duplicate messages */ int new_count = 0; int dup_count = 0; GSList* new_lines = NULL; for (int i = 0; i < 1000; i++) { char sid[48]; if (i < 500) { /* Duplicate */ snprintf(sid, sizeof(sid), "merge-sid-%d", i); } else { /* New */ snprintf(sid, sizeof(sid), "merge-sid-new-%d", i); } if (g_hash_table_contains(seen, sid)) { dup_count++; } else { g_hash_table_add(seen, g_strdup(sid)); new_count++; /* Would add to new_lines in real export */ } } assert_int_equal(dup_count, 500); assert_int_equal(new_count, 500); assert_int_equal((int)g_hash_table_size(seen), 1500); g_slist_free_full(existing, (GDestroyNotify)ff_parsed_line_free); g_slist_free(new_lines); g_hash_table_destroy(seen); unlink(tmppath); g_date_time_unref(base); } void test_stress_mixed_types_and_encodings(void** state) { /* Write 3000 messages cycling through all message types and encodings */ const char* types[] = { "chat", "muc", "mucpm" }; const char* encs[] = { "none", "omemo", "otr", "pgp", "ox" }; char tmppath[] = "/tmp/profstress_mixed_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < 3000; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); const char* t = types[i % 3]; const char* e = encs[i % 5]; char sid[32]; snprintf(sid, sizeof(sid), "mixed-%d", i); ff_write_line(fp, ts_str, t, e, sid, NULL, NULL, "sender@j.org", "res", "recv@j.org", NULL, -1, "mixed type/enc test"); } fclose(fp); g_date_time_unref(base); /* Parse and verify type/enc distribution */ fp = fopen(tmppath, "r"); assert_non_null(fp); int counts[3] = { 0 }; /* chat, muc, mucpm */ int enc_counts[5] = { 0 }; /* none, omemo, otr, pgp, ox */ int parsed = 0; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (!pl) continue; parsed++; if (g_strcmp0(pl->type, "chat") == 0) counts[0]++; else if (g_strcmp0(pl->type, "muc") == 0) counts[1]++; else if (g_strcmp0(pl->type, "mucpm") == 0) counts[2]++; if (g_strcmp0(pl->enc, "none") == 0) enc_counts[0]++; else if (g_strcmp0(pl->enc, "omemo") == 0) enc_counts[1]++; else if (g_strcmp0(pl->enc, "otr") == 0) enc_counts[2]++; else if (g_strcmp0(pl->enc, "pgp") == 0) enc_counts[3]++; else if (g_strcmp0(pl->enc, "ox") == 0) enc_counts[4]++; ff_parsed_line_free(pl); } fclose(fp); unlink(tmppath); assert_int_equal(parsed, 3000); assert_int_equal(counts[0], 1000); assert_int_equal(counts[1], 1000); assert_int_equal(counts[2], 1000); assert_int_equal(enc_counts[0], 600); assert_int_equal(enc_counts[1], 600); assert_int_equal(enc_counts[2], 600); assert_int_equal(enc_counts[3], 600); assert_int_equal(enc_counts[4], 600); } /* ================================================================ * LMC correction chains under load * ================================================================ */ void test_stress_lmc_chain_deep(void** state) { /* Build a chain of 90 corrections (under FF_MAX_LMC_DEPTH=100). * Write: msg0 (stanza_id=c0), msg1 (stanza_id=c1, corrects=c0), ..., * msg89 (stanza_id=c89, corrects=c88). * After LMC resolution, result should have 1 message with text from c89. */ const int chain_len = 90; char tmppath[] = "/tmp/profstress_lmc_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); for (int i = 0; i < chain_len; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32], rid[32]; snprintf(sid, sizeof(sid), "c%d", i); const char* replace = NULL; if (i > 0) { snprintf(rid, sizeof(rid), "c%d", i - 1); replace = rid; } char body[64]; snprintf(body, sizeof(body), "correction-v%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, NULL, replace, "alice@x.org", NULL, NULL, NULL, -1, body); } fclose(fp); g_date_time_unref(base); /* Read all lines, parse, build parsed list */ fp = fopen(tmppath, "r"); assert_non_null(fp); GSList* parsed_lines = NULL; while (1) { char* buf = ff_readline(fp, NULL); if (!buf) break; if (buf[0] == '#' || buf[0] == '\0') { free(buf); continue; } ff_parsed_line_t* pl = ff_parse_line(buf); free(buf); if (pl) parsed_lines = g_slist_prepend(parsed_lines, pl); } fclose(fp); unlink(tmppath); parsed_lines = g_slist_reverse(parsed_lines); assert_int_equal((int)g_slist_length(parsed_lines), chain_len); /* The _ff_apply_lmc is static, so we test its effect indirectly: * We use ff_parsed_to_profmessage and manual chain resolution logic * that mirrors what _ff_apply_lmc does. */ /* Build id_map */ GHashTable* id_map = g_hash_table_new(g_str_hash, g_str_equal); for (GSList* l = parsed_lines; l; l = l->next) { ff_parsed_line_t* pl = l->data; if (pl->stanza_id && strlen(pl->stanza_id) > 0) g_hash_table_insert(id_map, pl->stanza_id, pl); } /* Find root of the chain */ ff_parsed_line_t* first = parsed_lines->data; assert_null(first->replace_id); assert_string_equal(first->stanza_id, "c0"); /* Follow the chain from last correction */ ff_parsed_line_t* last = g_slist_last(parsed_lines)->data; assert_string_equal(last->stanza_id, "c89"); assert_string_equal(last->message, "correction-v89"); /* Verify chain integrity */ int depth = 0; ff_parsed_line_t* cur = last; while (cur->replace_id && strlen(cur->replace_id) > 0 && depth < FF_MAX_LMC_DEPTH) { ff_parsed_line_t* parent = g_hash_table_lookup(id_map, cur->replace_id); assert_non_null(parent); cur = parent; depth++; } assert_int_equal(depth, chain_len - 1); assert_string_equal(cur->stanza_id, "c0"); g_hash_table_destroy(id_map); g_slist_free_full(parsed_lines, (GDestroyNotify)ff_parsed_line_free); } void test_stress_lmc_many_corrections(void** state) { /* 500 original messages, each corrected once = 1000 lines total. * After LMC, should produce 500 messages with corrected text. */ const int n_originals = 500; char tmppath[] = "/tmp/profstress_lmcmany_XXXXXX"; int fd = mkstemp(tmppath); assert_true(fd >= 0); FILE* fp = fdopen(fd, "w"); assert_non_null(fp); GDateTime* base = g_date_time_new_utc(2026, 1, 1, 0, 0, 0); /* Write originals */ for (int i = 0; i < n_originals; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)i); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32]; snprintf(sid, sizeof(sid), "orig-%d", i); char body[64]; snprintf(body, sizeof(body), "original-%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, NULL, NULL, "alice@x.org", NULL, NULL, NULL, -1, body); } /* Write corrections */ for (int i = 0; i < n_originals; i++) { GDateTime* ts = g_date_time_add_seconds(base, (double)(n_originals + i)); auto_gchar gchar* ts_str = g_date_time_format_iso8601(ts); g_date_time_unref(ts); char sid[32], rid[32]; snprintf(sid, sizeof(sid), "corr-%d", i); snprintf(rid, sizeof(rid), "orig-%d", i); char body[64]; snprintf(body, sizeof(body), "corrected-%d", i); ff_write_line(fp, ts_str, "chat", "none", sid, NULL, rid, "alice@x.org", NULL, NULL, NULL, -1, body); } fclose(fp); g_date_time_unref(base); /* Parse and verify structure */ fp = fopen(tmppath, "r"); assert_non_null(fp); int total_parsed = 0; int originals_found = 0; int corrections_found = 0; while (1) { auto_char char* buf = ff_readline(fp, NULL); if (!buf) break; // ff_parse_line already rejects empty/comment lines, no need to pre-filter. ff_parsed_line_t* pl = ff_parse_line(buf); if (pl) { total_parsed++; if (pl->replace_id && strlen(pl->replace_id) > 0) corrections_found++; else originals_found++; ff_parsed_line_free(pl); } } fclose(fp); unlink(tmppath); assert_int_equal(total_parsed, n_originals * 2); assert_int_equal(originals_found, n_originals); assert_int_equal(corrections_found, n_originals); }