Files
profanity/tests/unittests/test_database_stress.c
jabber.developer2 8868d58920 fix(review): address PR #94 review #37/#38 reviewer comments
Parser (src/database_flatfile_parser.c):
- ff_jid_to_dir: drop GString+step1, in-place mutate str_replace result
- ff_ensure_dir: g_file_test(G_FILE_TEST_IS_SYMLINK) instead of g_lstat
- ff_unescape_*: early-continue for non-escape branch (less nesting)
- ff_readline: strdup("") instead of malloc(1)+'\0' for skip-line return
- ff_write_line: auto_char safe_msg + single fprintf (drop GString full_line)
- ff_parse_line: early return on missing closing bracket;
  extract metadata-parts loop into _ff_parse_meta_parts helper

Verify (src/database_flatfile_verify.c):
- Split monolithic ff_verify_integrity into _collect_contact_dirs,
  _check_permissions, _first_pass, _lmc_pass, _verify_contact_dir
- _issue_new constructor; g_new0 throughout
- Separate seen_stanza_ids and seen_archive_ids tables (no cross-kind
  duplicate confusion)
- ff_skip_bom in second pass (drop manual fgetc x3)
- log_debug when skipping a contact dir without history.log
- Doc comment on ff_verify_integrity in header

SQLite (src/database_sqlite.c):
- Doc block on ChatLogs schema (replaces_db_id <-> replaced_by_db_id
  invariant maintained by AFTER INSERT trigger)
- log_error when _get_db_filename returns NULL during init

Flatfile dispatch (src/database_flatfile.c):
- Replace if/else with is_outgoing ternary for log-path contact pick

Tests:
- 6 new invalid-date parser tests: empty / partial ISO / garbage /
  impossible calendar / negative year / far future timestamp
- New functional test message_db_history_multi_resource verifying
  same-JID-different-resource history rehydration preserves resources
- test_database_stress.c LMC chain loop: auto_char buf, drop redundant
  comment/empty pre-filter (ff_parse_line handles those)
2026-04-30 10:53:03 +03:00

1130 lines
34 KiB
C

// 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 <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/stat.h>
#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);
}