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)
1130 lines
34 KiB
C
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);
|
|
}
|