Compare commits

..

4 Commits

Author SHA1 Message Date
1aaa382d5e fix(verify): per-contact context in output, demote duplicate stanza-id to debug
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 2m39s
CI Code / Linux (debian) (pull_request) Successful in 4m39s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m51s
CI Code / Linux (arch) (pull_request) Successful in 5m40s
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 36s
CI Code / Code Coverage (push) Successful in 2m42s
CI Code / Linux (debian) (push) Successful in 4m43s
CI Code / Linux (ubuntu) (push) Successful in 4m58s
CI Code / Linux (arch) (push) Successful in 5m44s
2026-05-06 17:02:27 +03:00
3f36c303c2 feat(history): flat-file backend with bidirectional SQLite migration
All checks were successful
CI Code / Check spelling (push) Successful in 21s
CI Code / Check coding style (push) Successful in 31s
CI Code / Code Coverage (push) Successful in 2m21s
CI Code / Linux (ubuntu) (push) Successful in 4m30s
CI Code / Linux (debian) (push) Successful in 6m43s
CI Code / Linux (arch) (push) Successful in 10m8s
A flat-file alternative to the SQLite chatlog backend with runtime
switching, full migration tooling, integrity verification, and a
synthetic load harness. SQLite remains the default; both backends share
one dispatch layer (db_backend_t vtable) so callers don't change.

Storage layout
- Per-contact append-only `flatlog/<account>/<contact>/history.log`
  under XDG_DATA_HOME, one line per message
- Single-line file header with embedded format-version marker
  (FLATFILE_FORMAT_VERSION); reader warns on missing or mismatched
  marker, writer and checker stay in sync via preprocessor
  stringification
- Deterministic key=value metadata (`id`, `aid`, `corrects`, `to`,
  `to_res`, `read`) plus escaped body \u2014 `\|`, `\]`, `\\`, `\n`, `\r`
  literals prevent log injection
- Sparse byte-offset index (FF_INDEX_STEP=500) per contact for
  O(log n) time-range lookups; rebuilt on inode / size / mtime
  change, extended in-place when the file just grew
- Per-contact GHashTable caches for archive_id presence and
  stanza_id \u2192 from_jid mapping (O(1) MAM dedup, O(1) LMC sender
  validation)

Hardening
- Path-traversal protection: JID directory name normalisation
  (`@` \u2192 `_at_`, slashes and `..` rejected at construction); every
  per-contact path is anchored under the account's flatlog/
  directory and validated before open
- Symlink-attack protection: every fopen / open uses O_NOFOLLOW; on
  ELOOP the operation aborts with an error rather than following
- Filesystem permissions: log files created with mode 0600,
  directories with mode 0700; both enforced at creation, verified
  on each open and reported on drift by `/history verify`
- Atomic crash-safe export: write to a temp file via mkstemp (mode
  0600, random suffix, no name collisions between concurrent
  exports), fsync, then rename \u2014 partial state never replaces the
  live file
- Concurrency: advisory flock(LOCK_EX) held for the duration of
  every write, including append from live messages and full rewrite
  from export, so two profanity processes can't interleave bytes
  on the same log
- DoS / abuse guards:
    * FF_MAX_LINE_LEN = 10 MB \u2014 lines longer than this are rejected
      at read with a warning; the parser will not allocate
      unbounded memory for a single record
    * FF_MAX_LMC_DEPTH = 100 \u2014 `corrects:` chain walk stops at this
      depth and emits a warning, preventing a malicious correction
      cycle from spinning the apply pass
    * FF_VERSION_SCAN_MAX = 16 \u2014 header version probe never reads
      past 16 leading comment lines, even on garbage input
    * Empty / inverted byte-range early-return in page-up read path
      so a malformed time filter cannot cause an unbounded scan
    * Zero-entry index guard so a file whose every line failed to
      parse cannot cause a NULL deref on later page-up
- LMC sender validation: an incoming correction whose sender does
  not match the original message's sender is rejected at write
  time and surfaced via cons_show_error; a cycle in the apply pass
  is broken via a visited-set
- jid_create_from_bare_and_resource treats NULL, empty string, and
  the literal "(null)" as no resource and returns a bare jid;
  similar normalisation for barejid eliminates the legacy
  "user@host/(null)" artefact that leaked into stored fulljids
  whenever g_strdup_printf("%s", NULL) ran inside create_fulljid

Commands
- `/history switch sqlite|flatfile` \u2014 runtime backend swap, closes
  the old backend and opens the new one without reconnecting
- `/history export [<jid>]` \u2014 SQLite -> flat-file, merging with any
  existing flatlog (dedup keyed on a SHA-256 hash mixing stanza_id,
  timestamp, from_jid, body \u2014 robust against id reuse by older
  clients)
- `/history import [<jid>]` \u2014 flat-file -> SQLite, same merge
  semantics, runs inside a single SQLite transaction with rollback
  on per-contact failure
- `/history verify [<jid>]` \u2014 integrity check; emits a structured
  list of issues (ERROR / WARNING / INFO) per file:
    * file-level: missing log, wrong permissions (\u2260 0600), UTF-8
      BOM present, CRLF line endings, empty file
    * line-level: invalid UTF-8 (with byte offset), embedded
      control characters, unparsable lines, timestamps out of
      order, duplicate `id:` and `aid:` (tracked separately so a
      stanza/archive id collision isn't double-reported)
    * cross-line: broken `corrects:` references whose target id is
      not present in the file
- `/history backend` \u2014 show currently active backend
- Active backend indicator `[sqlite]` / `[flatfile]` in the status
  bar next to the JID
- Roster-JID autocomplete for verify / export / import
- export and import open a SQLite handle on demand when the
  flatfile backend is currently active, so migration works
  regardless of which backend is live

Tests
- Unit: database_export (parser round-trip, escape/unescape, dedup
  key stability, JID normalisation), database_stress (14 cases
  exercising rapid writes, large messages, deep LMC chains, MAM
  dedup, concurrent contacts)
- Functional: history persistence across reconnects, export /
  import round-trip with content equality, MUC migration,
  timestamp normalisation across timezones
- Bench harness P1\u2013P5 (synthetic load: bulk insert, time-range
  read, page-up scroll, MAM ingest, mixed workload) and failure
  modes F1\u2013F17 (page-up cursor and forward-iteration symmetry,
  oversized lines, MAM dedup, LMC depth and cycles, BOM/CRLF,
  missing log, empty file, mtime+inode flip, broken corrects, etc.)
- All bench tests integrate with the existing make targets and
  emit CSV rows for baseline comparison

Author: jabber.developer2 <jabber.developer2@jabber.space>
Reviewed-by: jabber.developer <jabber.developer@jabber.space>
2026-05-05 19:26:07 +00:00
0feacbc9da ci: simulate Pikaur flag duplication in Arch Linux CI
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 30s
CI Code / Code Coverage (pull_request) Successful in 3m1s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m31s
CI Code / Linux (debian) (pull_request) Successful in 7m36s
CI Code / Linux (arch) (pull_request) Successful in 9m52s
CI Code / Check spelling (push) Successful in 18s
CI Code / Check coding style (push) Successful in 32s
CI Code / Linux (debian) (push) Successful in 4m51s
CI Code / Linux (arch) (push) Successful in 5m35s
CI Code / Linux (ubuntu) (push) Successful in 6m36s
CI Code / Code Coverage (push) Successful in 7m21s
Inject system flags from /etc/makepkg.conf into the CI environment to
detect build collisions caused by Pikaur's configuration bug.

Pikaur's cascading logic causes flags from /etc/makepkg.conf to be
merged into the build environment. This creates collisions with flags
defined in the project's Makefile.am (e.g., duplicate -D_FORTIFY_SOURCE
definitions), which can cause builds to fail for users.

By exporting these flags in the CI environment, we ensure that any
code change that is sensitive to flag duplication will trigger a
failure in our Arch Linux CI matrix, preventing broken builds from
reaching users.

Implementation details:
- Detects Arch Linux via /etc/os-release.
- Uses a sed-based flattener to handle multi-line variables and
  trailing backslashes in makepkg.conf.
- Exports the flags to the shell environment so that 'configure'
  and 'make' inherit them naturally, maintaining parity with a
  real Pikaur session.
2026-04-21 10:17:44 +00:00
0722dc9e36 build(pikaur): Fix failure due to duplicated flag and warnings
All checks were successful
CI Code / Check spelling (pull_request) Successful in 20s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m42s
CI Code / Linux (debian) (pull_request) Successful in 6m47s
CI Code / Code Coverage (pull_request) Successful in 7m3s
CI Code / Linux (arch) (pull_request) Successful in 9m33s
CI Code / Check spelling (push) Successful in 20s
CI Code / Check coding style (push) Successful in 39s
CI Code / Code Coverage (push) Successful in 2m53s
CI Code / Linux (debian) (push) Successful in 4m8s
CI Code / Linux (ubuntu) (push) Successful in 4m35s
CI Code / Linux (arch) (push) Successful in 10m28s
2026-04-13 19:52:24 +00:00
15 changed files with 542 additions and 313 deletions

View File

@@ -204,6 +204,13 @@ case "$ARCH" in
""
)
source /etc/profile.d/debuginfod.sh 2>/dev/null || true
if grep -q 'ID=arch' /etc/os-release 2>/dev/null && [ -f /etc/makepkg.conf ]; then
echo "--> [Parity Mode] Simulating Pikaur collision..."
set -a
source /etc/makepkg.conf
set +a
fi
;;
darwin*)
# 4 configurations for parallel CI

View File

@@ -396,12 +396,11 @@ AC_SUBST([FORKPTY_LIB])
## Default parameters
AM_CFLAGS="$AM_CFLAGS -Wall -Wextra -Wformat=2 -Wno-format-zero-length"
AM_CFLAGS="$AM_CFLAGS -Wno-deprecated-declarations -Wno-unused-parameter -Wno-missing-field-initializers -Wno-sign-compare -Wno-cast-function-type"
AM_CFLAGS="$AM_CFLAGS -Wnull-dereference -Wpointer-arith"
AM_CFLAGS="$AM_CFLAGS -Wpointer-arith"
AM_CFLAGS="$AM_CFLAGS -Wimplicit-function-declaration"
AM_CFLAGS="$AM_CFLAGS -Wundef"
AM_CFLAGS="$AM_CFLAGS -Wfloat-equal -Wredundant-decls"
AM_CFLAGS="$AM_CFLAGS -fstack-protector-strong -fno-common"
AM_CFLAGS="$AM_CFLAGS -D_FORTIFY_SOURCE=2"
AM_CFLAGS="$AM_CFLAGS -std=gnu99 -ggdb3"
# GCC-specific warnings (not supported by clang) — test each one

View File

@@ -310,11 +310,9 @@ _verify_contact_dir(const char* cdir_path, GSList** issues)
// identify which contact the problem belongs to (every contact has its
// own history.log, so the bare filename is ambiguous).
auto_gchar gchar* dir_name = g_path_get_basename(cdir_path);
char* dir_unescaped = str_replace(dir_name, "_at_", "@");
auto_gchar gchar* basename_owned = g_strdup_printf("%s/history.log",
dir_unescaped ? dir_unescaped : dir_name);
free(dir_unescaped);
const char* basename = basename_owned;
auto_gcharv gchar** parts = g_strsplit(dir_name, "_at_", -1);
auto_gchar gchar* contact_jid = g_strjoinv("@", parts);
auto_gchar gchar* basename = g_strdup_printf("%s/history.log", contact_jid);
_check_permissions(filepath, basename, issues);

View File

@@ -510,10 +510,10 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_WARNING;
issue->file = g_strdup("chatlog.db");
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
from_a ? from_a : "?", to_a ? to_a : "?");
issue->line = id_a;
issue->message = g_strdup_printf("Timestamp out of order in %s↔%s: row %d (%s) > row %d (%s)",
from_a ? from_a : "?", to_a ? to_a : "?",
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
id_a, ts_a ? ts_a : "NULL",
id_b, ts_b ? ts_b : "NULL");
issues = g_slist_append(issues, issue);
@@ -537,10 +537,10 @@ _sqlite_verify_integrity(const gchar* const contact_barejid)
integrity_issue_t* issue = g_malloc0(sizeof(integrity_issue_t));
issue->level = INTEGRITY_ERROR;
issue->file = g_strdup("chatlog.db");
issue->file = g_strdup_printf("%s↔%s/chatlog.db",
from ? from : "?", to ? to : "?");
issue->line = id;
issue->message = g_strdup_printf("Broken LMC reference in %s↔%s: row %d references non-existent row %d",
from ? from : "?", to ? to : "?",
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
id, replaces_id);
issues = g_slist_append(issues, issue);
}

View File

@@ -70,9 +70,12 @@ const char*
bench_volume_name(bench_volume_t v)
{
switch (v) {
case BENCH_VOLUME_SMALL: return "small";
case BENCH_VOLUME_MEDIUM: return "medium";
case BENCH_VOLUME_MAX: return "max";
case BENCH_VOLUME_SMALL:
return "small";
case BENCH_VOLUME_MEDIUM:
return "medium";
case BENCH_VOLUME_MAX:
return "max";
}
return "?";
}

View File

@@ -24,8 +24,7 @@ char* bench_fmt_bytes(uint64_t bytes);
char* bench_fmt_ms(double ms);
// Resolve BENCH_VOLUME env: "small" / "medium" / "max" / unset.
typedef enum
{
typedef enum {
BENCH_VOLUME_SMALL, // 10k lines / 1 contact / 1 year
BENCH_VOLUME_MEDIUM, // 500k lines / 1 contact / 5 years
BENCH_VOLUME_MAX, // 5M lines / 1 contact / 10 years

View File

@@ -105,26 +105,25 @@ db_path_for(const char* account_jid)
// Schema (lifted from database_sqlite.c so the seeder can populate without
// going through _sqlite_init's full machinery)
static const char* SCHEMA_DDL =
"CREATE TABLE IF NOT EXISTS `ChatLogs` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
"`from_jid` TEXT NOT NULL, "
"`to_jid` TEXT NOT NULL, "
"`from_resource` TEXT, `to_resource` TEXT, "
"`message` TEXT, `timestamp` TEXT, `type` TEXT, "
"`stanza_id` TEXT, `archive_id` TEXT, "
"`encryption` TEXT, `marked_read` INTEGER, "
"`replace_id` TEXT, "
"`replaces_db_id` INTEGER, `replaced_by_db_id` INTEGER);"
"CREATE TABLE IF NOT EXISTS `DbVersion` ("
"`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE);"
"INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2');"
"CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`);"
"CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`);"
"CREATE TRIGGER IF NOT EXISTS update_corrected_message "
"AFTER INSERT ON ChatLogs FOR EACH ROW WHEN NEW.replaces_db_id IS NOT NULL "
"BEGIN UPDATE ChatLogs SET replaced_by_db_id = NEW.id "
"WHERE id = NEW.replaces_db_id; END;";
static const char* SCHEMA_DDL = "CREATE TABLE IF NOT EXISTS `ChatLogs` ("
"`id` INTEGER PRIMARY KEY AUTOINCREMENT, "
"`from_jid` TEXT NOT NULL, "
"`to_jid` TEXT NOT NULL, "
"`from_resource` TEXT, `to_resource` TEXT, "
"`message` TEXT, `timestamp` TEXT, `type` TEXT, "
"`stanza_id` TEXT, `archive_id` TEXT, "
"`encryption` TEXT, `marked_read` INTEGER, "
"`replace_id` TEXT, "
"`replaces_db_id` INTEGER, `replaced_by_db_id` INTEGER);"
"CREATE TABLE IF NOT EXISTS `DbVersion` ("
"`dv_id` INTEGER PRIMARY KEY, `version` INTEGER UNIQUE);"
"INSERT OR IGNORE INTO `DbVersion` (`version`) VALUES ('2');"
"CREATE INDEX IF NOT EXISTS ChatLogs_timestamp_IDX ON `ChatLogs` (`timestamp`);"
"CREATE INDEX IF NOT EXISTS ChatLogs_to_from_jid_IDX ON `ChatLogs` (`to_jid`, `from_jid`);"
"CREATE TRIGGER IF NOT EXISTS update_corrected_message "
"AFTER INSERT ON ChatLogs FOR EACH ROW WHEN NEW.replaces_db_id IS NOT NULL "
"BEGIN UPDATE ChatLogs SET replaced_by_db_id = NEW.id "
"WHERE id = NEW.replaces_db_id; END;";
// ---------------------------------------------------------------------------
// Seed: open sqlite3 directly, build schema, batched-INSERT N rows.
@@ -160,14 +159,15 @@ do_seed(const seed_opts_t* o)
if (sqlite3_exec(db, "BEGIN TRANSACTION;", NULL, NULL, &err) != SQLITE_OK) {
fprintf(stderr, "seed: BEGIN failed: %s\n", err);
sqlite3_free(err); sqlite3_close(db); return 2;
sqlite3_free(err);
sqlite3_close(db);
return 2;
}
const char* INSERT_SQL =
"INSERT INTO ChatLogs ("
"from_jid, to_jid, from_resource, to_resource, message, timestamp, type, "
"stanza_id, archive_id, encryption, marked_read, replace_id"
") VALUES (?, ?, ?, ?, ?, ?, 'chat', ?, ?, 'none', -1, ?);";
const char* INSERT_SQL = "INSERT INTO ChatLogs ("
"from_jid, to_jid, from_resource, to_resource, message, timestamp, type, "
"stanza_id, archive_id, encryption, marked_read, replace_id"
") VALUES (?, ?, ?, ?, ?, ?, 'chat', ?, ?, 'none', -1, ?);";
sqlite3_stmt* stmt = NULL;
if (sqlite3_prepare_v2(db, INSERT_SQL, -1, &stmt, NULL) != SQLITE_OK) {
fprintf(stderr, "seed: prepare failed: %s\n", sqlite3_errmsg(db));
@@ -180,7 +180,8 @@ do_seed(const seed_opts_t* o)
int64_t span_secs = (int64_t)5 * 365 * 24 * 3600; // 5 years
char* prev_sid_per_contact[1024] = { 0 };
int n_contacts = o->contacts > 0 ? o->contacts : 1;
if (n_contacts > 1024) n_contacts = 1024;
if (n_contacts > 1024)
n_contacts = 1024;
double t0 = bench_now_ms();
@@ -209,10 +210,14 @@ do_seed(const seed_opts_t* o)
sqlite3_bind_text(stmt, 5, body, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, iso, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 7, sid, -1, SQLITE_TRANSIENT);
if (aid) sqlite3_bind_text(stmt, 8, aid, -1, SQLITE_TRANSIENT);
else sqlite3_bind_null(stmt, 8);
if (is_lmc) sqlite3_bind_text(stmt, 9, prev_sid_per_contact[ci], -1, SQLITE_TRANSIENT);
else sqlite3_bind_null(stmt, 9);
if (aid)
sqlite3_bind_text(stmt, 8, aid, -1, SQLITE_TRANSIENT);
else
sqlite3_bind_null(stmt, 8);
if (is_lmc)
sqlite3_bind_text(stmt, 9, prev_sid_per_contact[ci], -1, SQLITE_TRANSIENT);
else
sqlite3_bind_null(stmt, 9);
if (sqlite3_step(stmt) != SQLITE_DONE) {
fprintf(stderr, "seed: insert failed at row %" PRId64 ": %s\n",
@@ -309,21 +314,29 @@ db_row_count(const char* db_path)
static int64_t
db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t* total_b)
{
sqlite3* da = NULL; sqlite3* db = NULL;
if (sqlite3_open(a_path, &da) != SQLITE_OK) return -1;
if (sqlite3_open(b_path, &db) != SQLITE_OK) { sqlite3_close(da); return -1; }
sqlite3* da = NULL;
sqlite3* db = NULL;
if (sqlite3_open(a_path, &da) != SQLITE_OK)
return -1;
if (sqlite3_open(b_path, &db) != SQLITE_OK) {
sqlite3_close(da);
return -1;
}
const char* SQL =
"SELECT IFNULL(from_jid,''), IFNULL(to_jid,''), IFNULL(message,''), "
"IFNULL(timestamp,''), IFNULL(type,''), IFNULL(stanza_id,''), "
"IFNULL(archive_id,''), IFNULL(encryption,''), IFNULL(replace_id,'') "
"FROM ChatLogs ORDER BY timestamp ASC, stanza_id ASC;";
sqlite3_stmt* sa = NULL; sqlite3_stmt* sb = NULL;
const char* SQL = "SELECT IFNULL(from_jid,''), IFNULL(to_jid,''), IFNULL(message,''), "
"IFNULL(timestamp,''), IFNULL(type,''), IFNULL(stanza_id,''), "
"IFNULL(archive_id,''), IFNULL(encryption,''), IFNULL(replace_id,'') "
"FROM ChatLogs ORDER BY timestamp ASC, stanza_id ASC;";
sqlite3_stmt* sa = NULL;
sqlite3_stmt* sb = NULL;
if (sqlite3_prepare_v2(da, SQL, -1, &sa, NULL) != SQLITE_OK
|| sqlite3_prepare_v2(db, SQL, -1, &sb, NULL) != SQLITE_OK) {
if (sa) sqlite3_finalize(sa);
if (sb) sqlite3_finalize(sb);
sqlite3_close(da); sqlite3_close(db);
if (sa)
sqlite3_finalize(sa);
if (sb)
sqlite3_finalize(sb);
sqlite3_close(da);
sqlite3_close(db);
return -1;
}
@@ -332,14 +345,16 @@ db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t*
while (1) {
int ra = sqlite3_step(sa);
int rb = sqlite3_step(sb);
if (ra != SQLITE_ROW && rb != SQLITE_ROW) break;
if (ra == SQLITE_ROW) na++;
if (rb == SQLITE_ROW) nb++;
if (ra != SQLITE_ROW && rb != SQLITE_ROW)
break;
if (ra == SQLITE_ROW)
na++;
if (rb == SQLITE_ROW)
nb++;
if (ra != rb) {
mismatches++;
if (shown < 5) {
fprintf(stderr, " diff: row count drift at ra_row=%" PRId64
" rb_row=%" PRId64 " ra=%d rb=%d\n", na, nb, ra, rb);
fprintf(stderr, " diff: row count drift at ra_row=%" PRId64 " rb_row=%" PRId64 " ra=%d rb=%d\n", na, nb, ra, rb);
shown++;
}
continue;
@@ -364,8 +379,10 @@ db_diff_full(const char* a_path, const char* b_path, int64_t* total_a, int64_t*
sqlite3_finalize(sb);
sqlite3_close(da);
sqlite3_close(db);
if (total_a) *total_a = na;
if (total_b) *total_b = nb;
if (total_a)
*total_a = na;
if (total_b)
*total_b = nb;
return mismatches;
}
@@ -399,15 +416,23 @@ cmd_seed(int argc, char** argv)
char* db_path = NULL;
for (int i = 0; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--db=", 5) == 0) db_path = g_strdup(a + 5);
else if (strncmp(a, "--rows=", 7) == 0) o.rows = strtoll(a + 7, NULL, 10);
else if (strncmp(a, "--contacts=", 11) == 0) o.contacts = atoi(a + 11);
else if (strncmp(a, "--lmc=", 6) == 0) o.lmc_pct = atoi(a + 6);
else if (strncmp(a, "--body=", 7) == 0) o.avg_body = strtoull(a + 7, NULL, 10);
else if (strncmp(a, "--seed=", 7) == 0) o.seed = strtoull(a + 7, NULL, 10);
else if (strncmp(a, "--account=", 10) == 0) o.account_jid = a + 10;
if (strncmp(a, "--db=", 5) == 0)
db_path = g_strdup(a + 5);
else if (strncmp(a, "--rows=", 7) == 0)
o.rows = strtoll(a + 7, NULL, 10);
else if (strncmp(a, "--contacts=", 11) == 0)
o.contacts = atoi(a + 11);
else if (strncmp(a, "--lmc=", 6) == 0)
o.lmc_pct = atoi(a + 6);
else if (strncmp(a, "--body=", 7) == 0)
o.avg_body = strtoull(a + 7, NULL, 10);
else if (strncmp(a, "--seed=", 7) == 0)
o.seed = strtoull(a + 7, NULL, 10);
else if (strncmp(a, "--account=", 10) == 0)
o.account_jid = a + 10;
}
if (!db_path) db_path = db_path_for(o.account_jid);
if (!db_path)
db_path = db_path_for(o.account_jid);
o.db_path = db_path;
int r = do_seed(&o);
g_free(db_path);
@@ -423,10 +448,14 @@ cmd_export(int argc, char** argv)
const char* volume = "export";
for (int i = 0; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--account=", 10) == 0) account = a + 10;
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
if (strncmp(a, "--account=", 10) == 0)
account = a + 10;
else if (strncmp(a, "--csv=", 6) == 0)
csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0)
label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0)
volume = a + 9;
}
if (!init_sqlite_backend(account))
return 2;
@@ -461,10 +490,14 @@ cmd_import(int argc, char** argv)
const char* volume = "import";
for (int i = 0; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--account=", 10) == 0) account = a + 10;
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
if (strncmp(a, "--account=", 10) == 0)
account = a + 10;
else if (strncmp(a, "--csv=", 6) == 0)
csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0)
label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0)
volume = a + 9;
}
if (!init_sqlite_backend(account))
return 2;
@@ -507,24 +540,36 @@ cmd_roundtrip(int argc, char** argv)
int do_diff = 0;
for (int i = 0; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--rows=", 7) == 0) rows = strtoll(a + 7, NULL, 10);
else if (strncmp(a, "--csv=", 6) == 0) csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0) label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0) volume = a + 9;
else if (strncmp(a, "--lmc=", 6) == 0) lmc = atoi(a + 6);
else if (strncmp(a, "--body=", 7) == 0) body = strtoull(a + 7, NULL, 10);
else if (strcmp(a, "--full-diff") == 0) do_diff = 1;
if (strncmp(a, "--rows=", 7) == 0)
rows = strtoll(a + 7, NULL, 10);
else if (strncmp(a, "--csv=", 6) == 0)
csv = a + 6;
else if (strncmp(a, "--label=", 8) == 0)
label = a + 8;
else if (strncmp(a, "--volume=", 9) == 0)
volume = a + 9;
else if (strncmp(a, "--lmc=", 6) == 0)
lmc = atoi(a + 6);
else if (strncmp(a, "--body=", 7) == 0)
body = strtoull(a + 7, NULL, 10);
else if (strcmp(a, "--full-diff") == 0)
do_diff = 1;
}
const char* account_a = "rt-a@bench.example";
const char* account_b = "rt-b@bench.example";
auto_gchar gchar* db_a = db_path_for(account_a);
auto_gchar gchar* db_b = db_path_for(account_b);
unlink(db_a); unlink(db_b);
auto_gchar gchar* wal_a = g_strdup_printf("%s-wal", db_a); unlink(wal_a);
auto_gchar gchar* shm_a = g_strdup_printf("%s-shm", db_a); unlink(shm_a);
auto_gchar gchar* wal_b = g_strdup_printf("%s-wal", db_b); unlink(wal_b);
auto_gchar gchar* shm_b = g_strdup_printf("%s-shm", db_b); unlink(shm_b);
unlink(db_a);
unlink(db_b);
auto_gchar gchar* wal_a = g_strdup_printf("%s-wal", db_a);
unlink(wal_a);
auto_gchar gchar* shm_a = g_strdup_printf("%s-shm", db_a);
unlink(shm_a);
auto_gchar gchar* wal_b = g_strdup_printf("%s-wal", db_b);
unlink(wal_b);
auto_gchar gchar* shm_b = g_strdup_printf("%s-shm", db_b);
unlink(shm_b);
fprintf(stderr, "===== roundtrip: rows=%" PRId64 " account_a=%s account_b=%s =====\n",
rows, account_a, account_b);
@@ -539,11 +584,13 @@ cmd_roundtrip(int argc, char** argv)
so.seed = 42;
so.account_jid = account_a;
double seed_ms_t0 = bench_now_ms();
if (do_seed(&so) != 0) return 2;
if (do_seed(&so) != 0)
return 2;
double seed_ms = bench_now_ms() - seed_ms_t0;
// 2) Export DB_A → flatlog under account_a
if (!init_sqlite_backend(account_a)) return 2;
if (!init_sqlite_backend(account_a))
return 2;
double export_t0 = bench_now_ms();
int exported = log_database_export_to_flatfile(NULL);
double export_ms = bench_now_ms() - export_t0;
@@ -552,7 +599,8 @@ cmd_roundtrip(int argc, char** argv)
// 3) Cross-mount the flatlog tree at account_b's expected location
const char* base = getenv("BENCH_DATA_DIR");
if (!base || !base[0]) base = "/tmp/cproof-bench-export";
if (!base || !base[0])
base = "/tmp/cproof-bench-export";
auto_gchar gchar* dir_a = g_strdup_printf("%s/flatlog/%s", base, ff_jid_to_dir(account_a));
auto_gchar gchar* dir_b = g_strdup_printf("%s/flatlog/%s", base, ff_jid_to_dir(account_b));
auto_gchar gchar* parent_b = g_path_get_dirname(dir_b);
@@ -565,7 +613,8 @@ cmd_roundtrip(int argc, char** argv)
}
// 4) Import flatlog → DB_B
if (!init_sqlite_backend(account_b)) return 2;
if (!init_sqlite_backend(account_b))
return 2;
double import_t0 = bench_now_ms();
int imported = log_database_import_from_flatfile(NULL);
double import_ms = bench_now_ms() - import_t0;
@@ -583,8 +632,7 @@ cmd_roundtrip(int argc, char** argv)
int64_t na = 0, nb = 0;
mismatches = db_diff_full(db_a, db_b, &na, &nb);
diff_ms = bench_now_ms() - diff_t0;
fprintf(stderr, " full content diff: a_rows=%" PRId64 " b_rows=%" PRId64
" mismatches=%" PRId64 " (%.2fs)\n",
fprintf(stderr, " full content diff: a_rows=%" PRId64 " b_rows=%" PRId64 " mismatches=%" PRId64 " (%.2fs)\n",
na, nb, mismatches, diff_ms / 1000.0);
}
@@ -614,13 +662,19 @@ cmd_roundtrip(int argc, char** argv)
static int
cmd_verify(int argc, char** argv)
{
const char* a = NULL; const char* b = NULL;
const char* a = NULL;
const char* b = NULL;
for (int i = 0; i < argc; i++) {
const char* x = argv[i];
if (strncmp(x, "--db-a=", 7) == 0) a = x + 7;
else if (strncmp(x, "--db-b=", 7) == 0) b = x + 7;
if (strncmp(x, "--db-a=", 7) == 0)
a = x + 7;
else if (strncmp(x, "--db-b=", 7) == 0)
b = x + 7;
}
if (!a || !b) {
usage();
return 2;
}
if (!a || !b) { usage(); return 2; }
int64_t na = 0, nb = 0;
int64_t m = db_diff_full(a, b, &na, &nb);
fprintf(stderr, "verify: a_rows=%" PRId64 " b_rows=%" PRId64 " mismatches=%" PRId64 "\n",
@@ -631,15 +685,23 @@ cmd_verify(int argc, char** argv)
int
main(int argc, char** argv)
{
if (argc < 2) { usage(); return 2; }
if (argc < 2) {
usage();
return 2;
}
const char* sub = argv[1];
int sub_argc = argc - 2;
char** sub_argv = argv + 2;
if (g_strcmp0(sub, "seed") == 0) return cmd_seed(sub_argc, sub_argv);
if (g_strcmp0(sub, "export") == 0) return cmd_export(sub_argc, sub_argv);
if (g_strcmp0(sub, "import") == 0) return cmd_import(sub_argc, sub_argv);
if (g_strcmp0(sub, "roundtrip") == 0) return cmd_roundtrip(sub_argc, sub_argv);
if (g_strcmp0(sub, "verify") == 0) return cmd_verify(sub_argc, sub_argv);
if (g_strcmp0(sub, "seed") == 0)
return cmd_seed(sub_argc, sub_argv);
if (g_strcmp0(sub, "export") == 0)
return cmd_export(sub_argc, sub_argv);
if (g_strcmp0(sub, "import") == 0)
return cmd_import(sub_argc, sub_argv);
if (g_strcmp0(sub, "roundtrip") == 0)
return cmd_roundtrip(sub_argc, sub_argv);
if (g_strcmp0(sub, "verify") == 0)
return cmd_verify(sub_argc, sub_argv);
usage();
return 2;
}

View File

@@ -51,8 +51,8 @@
#include "config/account.h"
#include "database_flatfile.h"
#define ACCOUNT_JID "fail@bench.example"
#define CONTACT_JID "peer@bench.example"
#define ACCOUNT_JID "fail@bench.example"
#define CONTACT_JID "peer@bench.example"
// ---------------------------------------------------------------------------
// Test framework
@@ -109,7 +109,8 @@ typedef struct
static void
verify_summary_free(verify_summary_t* s)
{
if (!s) return;
if (!s)
return;
g_free(s->first_err);
g_free(s->first_warn);
}
@@ -124,15 +125,18 @@ run_verify(verify_summary_t* out)
out->total = g_slist_length(issues);
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* i = (integrity_issue_t*)l->data;
if (!i) continue;
if (!i)
continue;
switch (i->level) {
case INTEGRITY_ERROR:
out->errors++;
if (!out->first_err) out->first_err = g_strdup(i->message ? i->message : "");
if (!out->first_err)
out->first_err = g_strdup(i->message ? i->message : "");
break;
case INTEGRITY_WARNING:
out->warnings++;
if (!out->first_warn) out->first_warn = g_strdup(i->message ? i->message : "");
if (!out->first_warn)
out->first_warn = g_strdup(i->message ? i->message : "");
break;
case INTEGRITY_INFO:
out->infos++;
@@ -148,7 +152,10 @@ report(fail_ctx_t* ctx, const char* tag, gboolean ok, double wall_ms,
{
fprintf(stderr, " %s %-3s %.1fms %s\n",
ok ? "PASS" : "FAIL", tag, wall_ms, note ? note : "");
if (ok) ctx->passed++; else ctx->failed++;
if (ok)
ctx->passed++;
else
ctx->failed++;
if (ctx->csv_path) {
struct stat st;
uint64_t sz = (path && stat(path, &st) == 0) ? (uint64_t)st.st_size : 0;
@@ -182,7 +189,8 @@ write_normal_line(FILE* fp, int seq, const char* body)
static void
test_F1(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F1")) return;
if (!test_enabled(ctx->tests, "F1"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F1");
// Write 10 normal lines; chop the trailing \n off the last.
@@ -227,7 +235,8 @@ test_F1(fail_ctx_t* ctx)
static void
test_F2(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F2")) return;
if (!test_enabled(ctx->tests, "F2"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F2");
FILE* fp = fopen(path, "w");
@@ -262,14 +271,17 @@ test_F2(fail_ctx_t* ctx)
static void
test_F3(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F3")) return;
if (!test_enabled(ctx->tests, "F3"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F3");
FILE* fp = fopen(path, "w");
fprintf(fp, "%s", FLATFILE_HEADER);
write_normal_line(fp, 0, "before bom");
// Inject BOM bytes at the start of a line in the middle of the file.
fputc(0xEF, fp); fputc(0xBB, fp); fputc(0xBF, fp);
fputc(0xEF, fp);
fputc(0xBB, fp);
fputc(0xBF, fp);
fprintf(fp,
"2025-06-15T12:00:01Z [chat|none|id:after-bom] peer@bench.example/phone: after bom\n");
write_normal_line(fp, 2, "trailing");
@@ -296,7 +308,8 @@ test_F3(fail_ctx_t* ctx)
static void
test_F7(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F7")) return;
if (!test_enabled(ctx->tests, "F7"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F7");
// A: stanza_id="A", no replace
@@ -337,7 +350,8 @@ test_F7(fail_ctx_t* ctx)
static void
test_F8(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F8")) return;
if (!test_enabled(ctx->tests, "F8"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F8");
FILE* fp = fopen(path, "w");
@@ -376,7 +390,8 @@ test_F8(fail_ctx_t* ctx)
static void
test_F9(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F9")) return;
if (!test_enabled(ctx->tests, "F9"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F9");
FILE* fp = fopen(path, "w");
@@ -412,7 +427,8 @@ test_F9(fail_ctx_t* ctx)
static void
test_F10(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F10")) return;
if (!test_enabled(ctx->tests, "F10"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F10");
FILE* fp = fopen(path, "w");
@@ -450,7 +466,8 @@ test_F10(fail_ctx_t* ctx)
static void
test_F11(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F11")) return;
if (!test_enabled(ctx->tests, "F11"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F11");
FILE* fp = fopen(path, "w");
@@ -488,7 +505,8 @@ test_F11(fail_ctx_t* ctx)
static void
test_F12(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F12")) return;
if (!test_enabled(ctx->tests, "F12"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F12");
FILE* fp = fopen(path, "w");
@@ -518,7 +536,8 @@ test_F12(fail_ctx_t* ctx)
static void
test_F14(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F14")) return;
if (!test_enabled(ctx->tests, "F14"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F14");
FILE* fp = fopen(path, "w");
@@ -563,7 +582,8 @@ test_F14(fail_ctx_t* ctx)
static void
test_F15(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F15")) return;
if (!test_enabled(ctx->tests, "F15"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F15");
FILE* fp = fopen(path, "w");
fclose(fp);
@@ -600,7 +620,8 @@ test_F15(fail_ctx_t* ctx)
static void
test_F16(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F16")) return;
if (!test_enabled(ctx->tests, "F16"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F16");
// Write enough messages that we cross at least 3 index entries
@@ -715,7 +736,8 @@ test_F16(fail_ctx_t* ctx)
static void
test_F17(fail_ctx_t* ctx)
{
if (!test_enabled(ctx->tests, "F17")) return;
if (!test_enabled(ctx->tests, "F17"))
return;
auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F17");
const int total_msgs = 2000;
@@ -812,9 +834,12 @@ main(int argc, char** argv)
for (int i = 1; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--tmp=", 6) == 0) ctx.tmp_dir = a + 6;
else if (strncmp(a, "--csv=", 6) == 0) ctx.csv_path = a + 6;
else if (strncmp(a, "--tests=", 8) == 0) ctx.tests = a + 8;
if (strncmp(a, "--tmp=", 6) == 0)
ctx.tmp_dir = a + 6;
else if (strncmp(a, "--csv=", 6) == 0)
ctx.csv_path = a + 6;
else if (strncmp(a, "--tests=", 8) == 0)
ctx.tests = a + 8;
else {
fprintf(stderr, "unknown option: %s\n", a);
return 2;

View File

@@ -68,9 +68,12 @@ parse_args(int argc, char** argv, cli_t* c)
c->tests = "all";
for (int i = 1; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--tmp=", 6) == 0) c->tmp_dir = a + 6;
else if (strncmp(a, "--csv=", 6) == 0) c->csv_path = a + 6;
else if (strncmp(a, "--tests=", 8) == 0) c->tests = a + 8;
if (strncmp(a, "--tmp=", 6) == 0)
c->tmp_dir = a + 6;
else if (strncmp(a, "--csv=", 6) == 0)
c->csv_path = a + 6;
else if (strncmp(a, "--tests=", 8) == 0)
c->tests = a + 8;
else {
fprintf(stderr, "unknown option: %s\n", a);
return -1;
@@ -95,8 +98,7 @@ test_enabled(const char* list, const char* name)
// ---------------------------------------------------------------------------
// Body factories
typedef enum
{
typedef enum {
PAT_FILLER, // deterministic alpha-num
PAT_EMBEDDED_LF, // filler with newlines every 100 bytes
PAT_EMBEDDED_PIPE, // filler with '|' sprinkled
@@ -155,7 +157,8 @@ typedef struct
static void
roundtrip_cleanup(roundtrip_result_t* r)
{
if (!r) return;
if (!r)
return;
if (r->path && r->path[0])
unlink(r->path);
g_free(r->path);
@@ -167,7 +170,8 @@ write_n_messages(const char* path, int n, size_t body_len, pattern_t pat,
int64_t* bytes_written_out)
{
FILE* fp = fopen(path, "w");
if (!fp) return -1;
if (!fp)
return -1;
fprintf(fp, "%s", FLATFILE_HEADER);
GDateTime* base = g_date_time_new_utc(2025, 6, 15, 12, 0, 0.0);
@@ -176,7 +180,7 @@ write_n_messages(const char* path, int n, size_t body_len, pattern_t pat,
auto_gchar gchar* iso = g_date_time_format_iso8601(t);
g_date_time_unref(t);
auto_gchar gchar* sid = g_strdup_printf("long-%d-%08x", i,
(unsigned)(rand_r(&(unsigned){i + 1})));
(unsigned)(rand_r(&(unsigned){ i + 1 })));
auto_gchar gchar* body = make_body(body_len, pat);
ff_write_line(fp, iso, "chat", "none",
sid, NULL, NULL,
@@ -200,7 +204,8 @@ read_and_validate(const char* path, size_t expected_body_len, int n,
int64_t* parsed_out, int64_t* mismatched_out, int64_t* parse_fail_out)
{
FILE* fp = fopen(path, "r");
if (!fp) return -1;
if (!fp)
return -1;
ff_skip_bom(fp);
int64_t parsed = 0, mismatched = 0, failures = 0;
@@ -224,9 +229,12 @@ read_and_validate(const char* path, size_t expected_body_len, int n,
ff_parsed_line_free(pl);
}
fclose(fp);
if (parsed_out) *parsed_out = parsed;
if (mismatched_out) *mismatched_out = mismatched;
if (parse_fail_out) *parse_fail_out = failures;
if (parsed_out)
*parsed_out = parsed;
if (mismatched_out)
*mismatched_out = mismatched;
if (parse_fail_out)
*parse_fail_out = failures;
(void)n;
return 0;
}
@@ -257,7 +265,8 @@ static void
csv_emit(const char* csv_path, const char* tag, size_t body_len, int n,
const roundtrip_result_t* r, const char* note)
{
if (!csv_path) return;
if (!csv_path)
return;
auto_gchar gchar* full_note = g_strdup_printf(
"n=%d body=%zu write=%.1fms read=%.1fms parsed=%" PRId64
" mismatch=%" PRId64 " parse_fail=%" PRId64 " %s",
@@ -337,8 +346,11 @@ run_oversized(const char* tmp, const char* csv)
}
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (pl) { parsed++; ff_parsed_line_free(pl); }
else failures++;
if (pl) {
parsed++;
ff_parsed_line_free(pl);
} else
failures++;
}
fclose(fp);
double read_ms = bench_now_ms() - t0;
@@ -373,7 +385,8 @@ run_pagination_with_huge(const char* tmp, const char* csv)
int huge_indices[5] = { 950, 970, 985, 992, 998 }; // five 1MB bodies near end
FILE* fp = fopen(path, "w");
if (!fp) return;
if (!fp)
return;
fprintf(fp, "%s", FLATFILE_HEADER);
GDateTime* base = g_date_time_new_utc(2025, 6, 15, 12, 0, 0.0);
for (int i = 0; i < total; i++) {
@@ -381,7 +394,11 @@ run_pagination_with_huge(const char* tmp, const char* csv)
auto_gchar gchar* iso = g_date_time_format_iso8601(t);
g_date_time_unref(t);
size_t blen = 100;
for (int k = 0; k < 5; k++) if (huge_indices[k] == i) { blen = MB; break; }
for (int k = 0; k < 5; k++)
if (huge_indices[k] == i) {
blen = MB;
break;
}
auto_gchar gchar* sid = g_strdup_printf("page-%d", i);
auto_gchar gchar* body = make_body(blen, PAT_FILLER);
ff_write_line(fp, iso, "chat", "none",
@@ -413,12 +430,17 @@ run_pagination_with_huge(const char* tmp, const char* csv)
int64_t parsed = 0;
char* buf;
while ((buf = ff_readline(fp, NULL)) != NULL) {
if (buf[0] == '\0' || buf[0] == '#') { free(buf); continue; }
if (buf[0] == '\0' || buf[0] == '#') {
free(buf);
continue;
}
ff_parsed_line_t* pl = ff_parse_line(buf);
free(buf);
if (!pl) continue;
if (!pl)
continue;
parsed++;
if (ring[rpos]) g_free(ring[rpos]);
if (ring[rpos])
g_free(ring[rpos]);
ring[rpos] = g_strdup(pl->message ? pl->message : "");
rpos = (rpos + 1) % 100;
ff_parsed_line_free(pl);
@@ -426,7 +448,8 @@ run_pagination_with_huge(const char* tmp, const char* csv)
fclose(fp);
double dt = bench_now_ms() - t0;
long rss = bench_peak_rss_kb();
for (int i = 0; i < 100; i++) g_free(ring[i]);
for (int i = 0; i < 100; i++)
g_free(ring[i]);
fprintf(stderr,
" L12 scrollback w/ 5×1MB in last 100 read=%.1fms parsed=%" PRId64 " rss=%ldKB\n",

View File

@@ -58,7 +58,7 @@ typedef struct
const char* csv_path;
const char* scenarios; // comma-separated list, or "all"
const char* account_jid;
int extend_lines; // for S5
int extend_lines; // for S5
} cli_t;
static void
@@ -77,11 +77,16 @@ parse_args(int argc, char** argv, cli_t* c)
cli_default(c);
for (int i = 1; i < argc; i++) {
const char* a = argv[i];
if (strncmp(a, "--data=", 7) == 0) c->data_dir = a + 7;
else if (strncmp(a, "--csv=", 6) == 0) c->csv_path = a + 6;
else if (strncmp(a, "--scenarios=", 12) == 0) c->scenarios = a + 12;
else if (strncmp(a, "--account=", 10) == 0) c->account_jid = a + 10;
else if (strncmp(a, "--extend-lines=", 15) == 0) c->extend_lines = atoi(a + 15);
if (strncmp(a, "--data=", 7) == 0)
c->data_dir = a + 7;
else if (strncmp(a, "--csv=", 6) == 0)
c->csv_path = a + 6;
else if (strncmp(a, "--scenarios=", 12) == 0)
c->scenarios = a + 12;
else if (strncmp(a, "--account=", 10) == 0)
c->account_jid = a + 10;
else if (strncmp(a, "--extend-lines=", 15) == 0)
c->extend_lines = atoi(a + 15);
else {
fprintf(stderr, "unknown option: %s\n", a);
return -1;
@@ -121,7 +126,8 @@ static int64_t
count_lines(const char* path)
{
FILE* fp = fopen(path, "r");
if (!fp) return -1;
if (!fp)
return -1;
int64_t n = 0;
int c;
while ((c = fgetc(fp)) != EOF) {
@@ -191,7 +197,8 @@ run_tail_access(const char* path, int drop_cache, long* peak_rss_kb, int64_t* li
ff_contact_state_t* state = ff_state_new(path);
if (!ff_state_ensure_fresh(state)) {
ff_state_free(state);
if (lines_read) *lines_read = 0;
if (lines_read)
*lines_read = 0;
return -1.0;
}
@@ -229,7 +236,8 @@ run_tail_access(const char* path, int drop_cache, long* peak_rss_kb, int64_t* li
if (!pl)
continue;
parsed++;
if (ring[ring_pos]) g_free(ring[ring_pos]);
if (ring[ring_pos])
g_free(ring[ring_pos]);
ring[ring_pos] = g_strdup(pl->message ? pl->message : "");
ring_pos = (ring_pos + 1) % TAIL_PAGE;
ff_parsed_line_free(pl);
@@ -297,8 +305,10 @@ run_first_build(const char* path, long* peak_rss_kb, size_t* idx_entries)
ff_contact_state_t* state = ff_state_new(path);
int ok = ff_state_ensure_fresh(state);
double dt = bench_now_ms() - t0;
if (idx_entries) *idx_entries = ok ? state->n_entries : 0;
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
if (idx_entries)
*idx_entries = ok ? state->n_entries : 0;
if (peak_rss_kb)
*peak_rss_kb = bench_peak_rss_kb();
ff_state_free(state);
return ok ? dt : -1.0;
}
@@ -330,7 +340,8 @@ run_incremental_extend(const char* path, int n_lines, long* peak_rss_kb,
int64_t before_size = -1;
{
struct stat st;
if (stat(path, &st) == 0) before_size = st.st_size;
if (stat(path, &st) == 0)
before_size = st.st_size;
}
GDateTime* now = g_date_time_new_now_utc();
for (int i = 0; i < n_lines; i++) {
@@ -359,7 +370,8 @@ run_incremental_extend(const char* path, int n_lines, long* peak_rss_kb,
else
*added_bytes = -1;
}
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
if (peak_rss_kb)
*peak_rss_kb = bench_peak_rss_kb();
fprintf(stderr, " S5: lines before=%zu after=%zu (delta=%zu)\n",
before_lines, state->total_lines,
@@ -380,7 +392,8 @@ _count_level(GSList* issues, integrity_level_t level)
int n = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* i = (integrity_issue_t*)l->data;
if (i && i->level == level) n++;
if (i && i->level == level)
n++;
}
return n;
}
@@ -406,7 +419,8 @@ run_verify(long* peak_rss_kb, int64_t* total_issues, int* errors, int* warnings,
int shown_err = 0, shown_warn = 0;
for (GSList* l = issues; l; l = l->next) {
integrity_issue_t* iss = (integrity_issue_t*)l->data;
if (!iss) continue;
if (!iss)
continue;
if (iss->level == INTEGRITY_ERROR && shown_err < 5) {
fprintf(stderr, " ERR %s:%d %s\n",
iss->file ? iss->file : "?", iss->line, iss->message ? iss->message : "");
@@ -419,11 +433,16 @@ run_verify(long* peak_rss_kb, int64_t* total_issues, int* errors, int* warnings,
}
g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free);
if (peak_rss_kb) *peak_rss_kb = bench_peak_rss_kb();
if (total_issues) *total_issues = total;
if (errors) *errors = e;
if (warnings) *warnings = w;
if (infos) *infos = i;
if (peak_rss_kb)
*peak_rss_kb = bench_peak_rss_kb();
if (total_issues)
*total_issues = total;
if (errors)
*errors = e;
if (warnings)
*warnings = w;
if (infos)
*infos = i;
return dt;
}
@@ -445,7 +464,8 @@ main(int argc, char** argv)
cli.data_dir, account_dir);
if (!g_file_test(contacts_root, G_FILE_TEST_IS_DIR)) {
fprintf(stderr, "ERROR: %s does not exist. Run gen_history first "
"(make sure --account matches).\n", contacts_root);
"(make sure --account matches).\n",
contacts_root);
return 2;
}
@@ -454,7 +474,8 @@ main(int argc, char** argv)
return 2;
const char* volume_name = getenv("BENCH_VOLUME");
if (!volume_name) volume_name = "medium";
if (!volume_name)
volume_name = "medium";
fprintf(stderr, "===== bench_runner: data=%s volume=%s =====\n", cli.data_dir, volume_name);

View File

@@ -34,7 +34,7 @@
#include "config/files.h"
#include "config/preferences.h"
#include "database.h"
#include "database_flatfile.h" // for ff_jid_to_dir() in stub
#include "database_flatfile.h" // for ff_jid_to_dir() in stub
#include "xmpp/xmpp.h"
#include "xmpp/jid.h"
#include "xmpp/message.h"
@@ -100,14 +100,50 @@ log_error(const char* const msg, ...)
va_end(ap);
}
void log_init(log_level_t filter, const char* const log_file) { (void)filter; (void)log_file; }
void log_close(void) {}
void log_msg(log_level_t level, const char* const area, const char* const msg) { (void)level; (void)area; (void)msg; }
const char* get_log_file_location(void) { return ""; }
log_level_t log_get_filter(void) { return PROF_LEVEL_INFO; }
int log_level_from_string(char* log_level, log_level_t* level) { (void)log_level; if (level) *level = PROF_LEVEL_INFO; return 0; }
void log_stderr_init(log_level_t level) { (void)level; }
void log_stderr_handler(void) {}
void
log_init(log_level_t filter, const char* const log_file)
{
(void)filter;
(void)log_file;
}
void
log_close(void)
{
}
void
log_msg(log_level_t level, const char* const area, const char* const msg)
{
(void)level;
(void)area;
(void)msg;
}
const char*
get_log_file_location(void)
{
return "";
}
log_level_t
log_get_filter(void)
{
return PROF_LEVEL_INFO;
}
int
log_level_from_string(char* log_level, log_level_t* level)
{
(void)log_level;
if (level)
*level = PROF_LEVEL_INFO;
return 0;
}
void
log_stderr_init(log_level_t level)
{
(void)level;
}
void
log_stderr_handler(void)
{
}
// ---------------------------------------------------------------------------
// prefs / files / xmpp / ui — never reached by the harness, but database_flatfile.c

View File

@@ -57,16 +57,14 @@
// ---------------------------------------------------------------------------
// CLI options
typedef enum
{
typedef enum {
SID_UUID,
SID_LIBPURPLE,
SID_CONVERSATIONS,
SID_MIXED,
} sid_mode_t;
typedef enum
{
typedef enum {
LEN_SHORT,
LEN_MIXED,
LEN_LONG,
@@ -109,20 +107,44 @@ opts_default(opts_t* o)
static int
parse_sid_mode(const char* s, sid_mode_t* out)
{
if (g_strcmp0(s, "uuid") == 0) { *out = SID_UUID; return 0; }
if (g_strcmp0(s, "libpurple") == 0) { *out = SID_LIBPURPLE; return 0; }
if (g_strcmp0(s, "conversations") == 0) { *out = SID_CONVERSATIONS; return 0; }
if (g_strcmp0(s, "mixed") == 0) { *out = SID_MIXED; return 0; }
if (g_strcmp0(s, "uuid") == 0) {
*out = SID_UUID;
return 0;
}
if (g_strcmp0(s, "libpurple") == 0) {
*out = SID_LIBPURPLE;
return 0;
}
if (g_strcmp0(s, "conversations") == 0) {
*out = SID_CONVERSATIONS;
return 0;
}
if (g_strcmp0(s, "mixed") == 0) {
*out = SID_MIXED;
return 0;
}
return -1;
}
static int
parse_len_profile(const char* s, len_profile_t* out)
{
if (g_strcmp0(s, "short") == 0) { *out = LEN_SHORT; return 0; }
if (g_strcmp0(s, "mixed") == 0) { *out = LEN_MIXED; return 0; }
if (g_strcmp0(s, "long") == 0) { *out = LEN_LONG; return 0; }
if (g_strcmp0(s, "extreme") == 0) { *out = LEN_EXTREME; return 0; }
if (g_strcmp0(s, "short") == 0) {
*out = LEN_SHORT;
return 0;
}
if (g_strcmp0(s, "mixed") == 0) {
*out = LEN_MIXED;
return 0;
}
if (g_strcmp0(s, "long") == 0) {
*out = LEN_LONG;
return 0;
}
if (g_strcmp0(s, "extreme") == 0) {
*out = LEN_EXTREME;
return 0;
}
return -1;
}
@@ -204,8 +226,26 @@ print_help(void)
// Body / stanza-id / resource generators (deterministic)
static const char* MSG_BANK_TINY[] = {
"ok", "yes", "no", "ack", "thx", "ttyl", "k", "lol", "+1", "yep", "nope",
"sure", "got it", "great", "afk", "brb", "bye", "hi", "hello", "hey",
"ok",
"yes",
"no",
"ack",
"thx",
"ttyl",
"k",
"lol",
"+1",
"yep",
"nope",
"sure",
"got it",
"great",
"afk",
"brb",
"bye",
"hi",
"hello",
"hey",
};
static const int MSG_BANK_TINY_N = sizeof(MSG_BANK_TINY) / sizeof(MSG_BANK_TINY[0]);
@@ -265,24 +305,35 @@ pick_body_len(uint64_t* rng, len_profile_t profile)
return 5 + (xorshift64(rng) % 96);
}
if (profile == LEN_LONG) {
if (r < 200) return 5 + (xorshift64(rng) % 96);
if (r < 600) return 100 + (xorshift64(rng) % 400);
if (r < 850) return 1000 + (xorshift64(rng) % 9000); // 1-10 KB
if (r < 980) return 10000 + (xorshift64(rng) % 90000); // 10-100 KB
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
if (r < 200)
return 5 + (xorshift64(rng) % 96);
if (r < 600)
return 100 + (xorshift64(rng) % 400);
if (r < 850)
return 1000 + (xorshift64(rng) % 9000); // 1-10 KB
if (r < 980)
return 10000 + (xorshift64(rng) % 90000); // 10-100 KB
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
}
if (profile == LEN_EXTREME) {
if (r < 100) return 5 + (xorshift64(rng) % 96);
if (r < 400) return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
if (r < 800) return 1000000 + (xorshift64(rng) % 4000000); // 1-5 MB
return 5000000 + (xorshift64(rng) % 4000000); // 5-9 MB
if (r < 100)
return 5 + (xorshift64(rng) % 96);
if (r < 400)
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
if (r < 800)
return 1000000 + (xorshift64(rng) % 4000000); // 1-5 MB
return 5000000 + (xorshift64(rng) % 4000000); // 5-9 MB
}
// LEN_MIXED — realistic distribution
if (r < 500) return 5 + (xorshift64(rng) % 96);
if (r < 900) return 100 + (xorshift64(rng) % 400);
if (r < 980) return 500 + (xorshift64(rng) % 4500); // 500B-5KB
if (r < 995) return 5000 + (xorshift64(rng) % 95000); // 5-100 KB
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
if (r < 500)
return 5 + (xorshift64(rng) % 96);
if (r < 900)
return 100 + (xorshift64(rng) % 400);
if (r < 980)
return 500 + (xorshift64(rng) % 4500); // 500B-5KB
if (r < 995)
return 5000 + (xorshift64(rng) % 95000); // 5-100 KB
return 100000 + (xorshift64(rng) % 900000); // 100KB-1MB
}
static char*
@@ -341,13 +392,15 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
sid_mode_t effective = mode;
if (mode == SID_MIXED) {
int r = (int)(xorshift64(rng) % 3);
effective = (r == 0) ? SID_UUID : (r == 1) ? SID_LIBPURPLE : SID_CONVERSATIONS;
effective = (r == 0) ? SID_UUID : (r == 1) ? SID_LIBPURPLE
: SID_CONVERSATIONS;
}
switch (effective) {
case SID_LIBPURPLE:
// Per-contact incremental — collides across contacts (intentional, mirrors libpurple).
return g_strdup_printf("%" PRId64, global_seq);
case SID_CONVERSATIONS: {
case SID_CONVERSATIONS:
{
// Conversations-style: 22 base32-ish chars
static const char alpha[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
char buf[24];
@@ -358,7 +411,8 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
}
case SID_UUID:
case SID_MIXED:
default: {
default:
{
// RFC4122-ish UUID v4 (deterministic from RNG, not cryptographic)
uint64_t a = xorshift64(rng);
uint64_t b = xorshift64(rng);
@@ -379,12 +433,12 @@ make_stanza_id(uint64_t* rng, sid_mode_t mode, int64_t global_seq, int contact_i
typedef struct
{
char* jid;
char** resources; // n = opts.resources_per_contact
char** resources; // n = opts.resources_per_contact
int n_resources;
char* dir;
char* path;
FILE* fp;
int64_t seq; // libpurple-style counter
int64_t seq; // libpurple-style counter
int64_t lines_written;
int64_t bytes_written;
char* last_stanza_id; // for LMC corrections

View File

@@ -61,7 +61,10 @@
#endif
/* Macro to wrap each test with setup/teardown functions */
#define PROF_FUNC_TEST(test) { #test, test, init_prof_test, close_prof_test, #test }
#define PROF_FUNC_TEST(test) \
{ \
#test, test, init_prof_test, close_prof_test, #test \
}
#ifdef HAVE_SQLITE
/* Custom init that sets a non-UTC timezone (POSIX TST-3 = UTC+3) */

View File

@@ -26,41 +26,41 @@
#define PORTS_PER_GROUP 50
/* Base port and fallback scan range for stabber */
#define BASE_PORT 5230
#define FALLBACK_PORT_RANGE (TEST_GROUPS * PORTS_PER_GROUP)
#define BASE_PORT 5230
#define FALLBACK_PORT_RANGE (TEST_GROUPS * PORTS_PER_GROUP)
/* Shutdown polling: interval and maximum attempts before escalating signals */
#define MS_TO_US 1000
#define SHUTDOWN_POLL_MS 50
#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */
#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */
#define QUIT_GRACE_MS 100 /* grace for /quit to be read from pty */
#define INIT_WAIT_MS 50 /* wait for child process to initialize */
#define INPUT_DELAY_MS 10 /* let profanity process input */
#define OUTPUT_POLL_MS 50 /* polling interval for output checks */
#define READ_TIMEOUT_MS 100 /* select() timeout for pty reads */
#define MS_TO_US 1000
#define SHUTDOWN_POLL_MS 50
#define SHUTDOWN_POLL_MAX 100 /* 100 x 50ms = 5s */
#define SIGTERM_POLL_MAX 20 /* 20 x 50ms = 1s */
#define QUIT_GRACE_MS 100 /* grace for /quit to be read from pty */
#define INIT_WAIT_MS 50 /* wait for child process to initialize */
#define INPUT_DELAY_MS 10 /* let profanity process input */
#define OUTPUT_POLL_MS 50 /* polling interval for output checks */
#define READ_TIMEOUT_MS 100 /* select() timeout for pty reads */
/* Expect timeouts (seconds) */
#define EXPECT_TIMEOUT_DEFAULT 30
#define EXPECT_TIMEOUT_CONNECT 60
#define EXPECT_TIMEOUT_DEFAULT 30
#define EXPECT_TIMEOUT_CONNECT 60
/* Test duration thresholds (seconds) */
#define SLOW_TEST_THRESHOLD_S 20
#define FAST_TEST_THRESHOLD_S 0.3
#define SLOW_TEST_THRESHOLD_S 20
#define FAST_TEST_THRESHOLD_S 0.3
/* Sentinel "infinity" for min-elapsed tracking (any real test is faster) */
#define MIN_ELAPSED_INIT 1e9
#define MIN_ELAPSED_INIT 1e9
/* Terminal dimensions for forkpty */
#define PTY_ROWS 24
#define PTY_COLS 300
#define PTY_ROWS 24
#define PTY_COLS 300
/* Preprocessor stringification (for setenv from numeric #define) */
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
char *config_orig;
char *data_orig;
char* config_orig;
char* data_orig;
int fd = 0;
int stub_port = BASE_PORT;
@@ -87,19 +87,19 @@ static int expect_timeout = EXPECT_TIMEOUT_DEFAULT;
/* Per-test wall-clock timer */
static struct timespec test_start_ts;
static const char *current_test_name;
static const char* current_test_name;
static double min_elapsed = MIN_ELAPSED_INIT;
static const char *min_test_name = "(none)";
static const char* min_test_name = "(none)";
/* Per-test threshold overrides (0 = use default) */
double prof_test_slow_threshold = 0;
double prof_test_fast_threshold = 0;
/* Timezone for profanity child process (see proftest.h) */
const char *prof_test_tz = "UTC";
const char* prof_test_tz = "UTC";
gboolean
_create_dir(const char *name)
_create_dir(const char* name)
{
struct stat sb;
@@ -117,14 +117,14 @@ _create_dir(const char *name)
}
gboolean
_mkdir_recursive(const char *dir)
_mkdir_recursive(const char* dir)
{
int i;
gboolean result = TRUE;
for (i = 1; i <= strlen(dir); i++) {
if (dir[i] == '/' || dir[i] == '\0') {
gchar *next_dir = g_strndup(dir, i);
gchar* next_dir = g_strndup(dir, i);
result = _create_dir(next_dir);
g_free(next_dir);
if (!result) {
@@ -139,7 +139,7 @@ _mkdir_recursive(const char *dir)
void
_create_config_dir(void)
{
GString *profanity_dir = g_string_new(xdg_config_home);
GString* profanity_dir = g_string_new(xdg_config_home);
g_string_append(profanity_dir, "/profanity");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -152,7 +152,7 @@ _create_config_dir(void)
void
_create_data_dir(void)
{
GString *profanity_dir = g_string_new(xdg_data_home);
GString* profanity_dir = g_string_new(xdg_data_home);
g_string_append(profanity_dir, "/profanity");
if (!_mkdir_recursive(profanity_dir->str)) {
@@ -165,7 +165,7 @@ _create_data_dir(void)
void
_create_chatlogs_dir(void)
{
GString *chatlogs_dir = g_string_new(xdg_data_home);
GString* chatlogs_dir = g_string_new(xdg_data_home);
g_string_append(chatlogs_dir, "/profanity/chatlogs");
if (!_mkdir_recursive(chatlogs_dir->str)) {
@@ -178,7 +178,7 @@ _create_chatlogs_dir(void)
void
_create_logs_dir(void)
{
GString *logs_dir = g_string_new(xdg_data_home);
GString* logs_dir = g_string_new(xdg_data_home);
g_string_append(logs_dir, "/profanity/logs");
if (!_mkdir_recursive(logs_dir->str)) {
@@ -191,12 +191,12 @@ _create_logs_dir(void)
void
_cleanup_dirs(void)
{
const char *group_env = getenv("PROF_TEST_GROUP");
const char* group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
int dir_id = (group >= 1 && group <= TEST_GROUPS) ? group : stub_port;
printf("[PROF_TEST] Cleaning up directories for group %d (dir_id %d)\n", group, dir_id);
char cmd[512];
snprintf(cmd, sizeof(cmd), "rm -rf ./test-files/%d", dir_id);
int res = system(cmd);
@@ -220,26 +220,26 @@ _read_output(int timeout_ms)
{
fd_set readfds;
struct timeval tv;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
tv.tv_sec = timeout_ms / MS_TO_US;
tv.tv_usec = (timeout_ms % MS_TO_US) * MS_TO_US;
int ret = select(fd + 1, &readfds, NULL, NULL, &tv);
if (ret <= 0) {
return ret;
}
size_t space = OUTPUT_BUF_SIZE - output_len - 1;
if (space <= 0) {
/* Buffer full, shift content */
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE/2, OUTPUT_BUF_SIZE/2);
output_len = OUTPUT_BUF_SIZE/2;
memmove(output_buffer, output_buffer + OUTPUT_BUF_SIZE / 2, OUTPUT_BUF_SIZE / 2);
output_len = OUTPUT_BUF_SIZE / 2;
space = OUTPUT_BUF_SIZE - output_len - 1;
}
ssize_t n = read(fd, output_buffer + output_len, space);
if (n > 0) {
output_len += n;
@@ -260,7 +260,7 @@ prof_start(void)
ws.ws_col = PTY_COLS;
ws.ws_xpixel = 0;
ws.ws_ypixel = 0;
/* Reset output buffer */
output_len = 0;
output_buffer[0] = '\0';
@@ -272,48 +272,48 @@ prof_start(void)
tzset();
child_pid = forkpty(&fd, NULL, NULL, &ws);
if (child_pid < 0) {
fd = -1;
return;
}
if (child_pid == 0) {
/* Child process */
setenv("COLUMNS", STRINGIFY(PTY_COLS), 1);
setenv("TERM", "xterm", 1);
execl("./profanity", "./profanity", "-l", "DEBUG", NULL);
/* If exec fails */
fprintf(stderr, "execl failed: %s\n", strerror(errno));
_exit(127);
}
/* Parent process */
/* Set non-blocking mode for reading */
int flags = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
/* Brief wait for process to initialize */
sleep_ms(INIT_WAIT_MS);
}
int
init_prof_test(void **state)
init_prof_test(void** state)
{
clock_gettime(CLOCK_MONOTONIC, &test_start_ts);
current_test_name = state && *state ? (const char *)*state : "unknown";
current_test_name = state && *state ? (const char*)*state : "unknown";
prof_test_slow_threshold = 0;
prof_test_fast_threshold = 0;
/* Get test group from environment for static resource allocation */
const char *group_env = getenv("PROF_TEST_GROUP");
const char* group_env = getenv("PROF_TEST_GROUP");
int group = group_env ? atoi(group_env) : 0;
/* Get build index for port offset (for parallel CI builds) */
const char *build_env = getenv("PROF_BUILD_INDEX");
const char* build_env = getenv("PROF_BUILD_INDEX");
int build_idx = build_env ? atoi(build_env) : 0;
/* Calculate port base: each build uses a different range of
* TEST_GROUPS * PORTS_PER_GROUP ports.
* Build 0 (local/default): 5230-5429, Full: 5230-5429, Minimal: 5430-5629, etc.
@@ -321,7 +321,7 @@ init_prof_test(void **state)
* or sequential run (no parallel builds), while Full/Minimal/NoEncrypt/Default
* are used in CI where they run in parallel. */
int port_base = BASE_PORT + ((build_idx > 0 ? build_idx - 1 : 0) * TEST_GROUPS * PORTS_PER_GROUP);
/* Each group gets a dedicated range of ports for parallel execution.
* Group 1: port_base..port_base+PORTS_PER_GROUP-1
* Group 2: port_base+PORTS_PER_GROUP..port_base+2*PORTS_PER_GROUP-1, etc.
@@ -353,7 +353,7 @@ init_prof_test(void **state)
}
}
}
if (!started) {
fprintf(stderr, "[PROF_TEST] ERROR: could not start stabber on any port\n");
return -1;
@@ -366,8 +366,8 @@ init_prof_test(void **state)
"./test-files/%d/xdg_config_home", dir_id);
snprintf(xdg_data_home, sizeof(xdg_data_home),
"./test-files/%d/xdg_data_home", dir_id);
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
printf("[PROF_TEST] Group %d using directories: config=%s, data=%s\n",
group, xdg_config_home, xdg_data_home);
config_orig = getenv("XDG_CONFIG_HOME");
@@ -418,7 +418,7 @@ init_prof_test(void **state)
}
int
close_prof_test(void **state)
close_prof_test(void** state)
{
if (fd > 0 && child_pid > 0) {
prof_input("/quit");
@@ -439,7 +439,10 @@ close_prof_test(void **state)
if (!exited) {
kill(child_pid, SIGTERM);
for (int i = 0; i < SIGTERM_POLL_MAX; i++) {
if (waitpid(child_pid, NULL, WNOHANG) != 0) { exited = 1; break; }
if (waitpid(child_pid, NULL, WNOHANG) != 0) {
exited = 1;
break;
}
sleep_ms(SHUTDOWN_POLL_MS);
}
if (!exited) {
@@ -462,7 +465,7 @@ close_prof_test(void **state)
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
double elapsed = (now.tv_sec - test_start_ts.tv_sec)
+ (now.tv_nsec - test_start_ts.tv_nsec) / 1e9;
+ (now.tv_nsec - test_start_ts.tv_nsec) / 1e9;
if (elapsed < min_elapsed) {
min_elapsed = elapsed;
min_test_name = current_test_name;
@@ -483,14 +486,14 @@ close_prof_test(void **state)
}
void
prof_input(const char *input)
prof_input(const char* input)
{
GString *inp_str = g_string_new(input);
GString* inp_str = g_string_new(input);
g_string_append(inp_str, "\r");
ssize_t _wn = write(fd, inp_str->str, inp_str->len);
(void)_wn;
g_string_free(inp_str, TRUE);
/* Small delay to let profanity process input */
sleep_ms(INPUT_DELAY_MS);
}
@@ -500,24 +503,24 @@ prof_input(const char *input)
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_exact(const char *text)
prof_output_exact(const char* text)
{
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(READ_TIMEOUT_MS) > 0) {
/* Keep reading while data available */
}
/* Check if text is in buffer */
if (strstr(output_buffer, text) != NULL) {
return 1;
}
sleep_ms(OUTPUT_POLL_MS);
}
return 0;
}
@@ -526,34 +529,34 @@ prof_output_exact(const char *text)
* Returns 1 if found, 0 if timeout.
*/
int
prof_output_regex(const char *pattern)
prof_output_regex(const char* pattern)
{
regex_t regex;
int ret;
ret = regcomp(&regex, pattern, REG_EXTENDED | REG_NOSUB);
if (ret != 0) {
return 0;
}
time_t start = time(NULL);
while (time(NULL) - start < expect_timeout) {
/* Read any available output */
while (_read_output(READ_TIMEOUT_MS) > 0) {
/* Keep reading while data available */
}
/* Check if pattern matches */
ret = regexec(&regex, output_buffer, 0, NULL, 0);
if (ret == 0) {
regfree(&regex);
return 1;
}
sleep_ms(OUTPUT_POLL_MS);
}
/* Timeout reached - log diagnostic info */
fprintf(stderr, "Timeout waiting for regex '%s' after %d seconds. Last output:\n", pattern, expect_timeout);
size_t len = strlen(output_buffer);
@@ -563,23 +566,21 @@ prof_output_regex(const char *pattern)
fprintf(stderr, "%s", output_buffer);
}
fprintf(stderr, "\n");
regfree(&regex);
return 0;
}
void
prof_connect_with_roster(const char *roster)
prof_connect_with_roster(const char* roster)
{
GString *roster_str = g_string_new(
GString* roster_str = g_string_new(
"<iq type='result' to='stabber@localhost/profanity'>"
"<query xmlns='jabber:iq:roster' ver='362'>"
);
"<query xmlns='jabber:iq:roster' ver='362'>");
g_string_append(roster_str, roster);
g_string_append(roster_str,
"</query>"
"</iq>"
);
"</query>"
"</iq>");
stbbr_for_query("jabber:iq:roster", roster_str->str);
g_string_free(roster_str, TRUE);
@@ -589,7 +590,7 @@ prof_connect_with_roster(const char *roster)
char connect_cmd[128];
snprintf(connect_cmd, sizeof(connect_cmd), "/connect stabber@localhost server 127.0.0.1 port %d tls disable auth legacy", stub_port);
prof_input(connect_cmd);
assert_true(prof_output_regex("password:"));
prof_input("password");
@@ -597,15 +598,14 @@ prof_connect_with_roster(const char *roster)
assert_true(prof_output_regex("Connecting as stabber@localhost"));
assert_true(prof_output_regex("logged in successfully"));
assert_true(prof_output_regex(".+online.+ \\(priority 0\\)\\."));
expect_timeout = EXPECT_TIMEOUT_CONNECT;
// Wait for presence stanza to be sent (content-based, not ID-based)
// Match the actual attribute order from stanza_attach_caps
assert_true(stbbr_received(
"<presence id=\"*\">"
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
"</presence>"
));
"<c xmlns=\"http://jabber.org/protocol/caps\" hash=\"sha-1\" node=\"http://profanity-im.github.io\" ver=\"*\"/>"
"</presence>"));
}
void
@@ -625,6 +625,5 @@ prof_connect(void)
{
prof_connect_with_roster(
"<item jid='buddy1@localhost' subscription='both' name='Buddy1'/>"
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>"
);
"<item jid='buddy2@localhost' subscription='both' name='Buddy2'/>");
}

View File

@@ -33,6 +33,6 @@ extern double prof_test_slow_threshold;
extern double prof_test_fast_threshold;
/* Timezone used for profanity child process (default "UTC") */
extern const char *prof_test_tz;
extern const char* prof_test_tz;
#endif