Compare commits

..

35 Commits

Author SHA1 Message Date
bdbd588d90 fix(verify): per-contact context in output, demote duplicate stanza-id to debug 2026-05-06 14:19:46 +03:00
26db671a38 fix(export): hash stanza_id with content instead of trusting it as sole dedup key
All checks were successful
CI Code / Check spelling (pull_request) Successful in 26s
CI Code / Check coding style (pull_request) Successful in 1m6s
CI Code / Code Coverage (pull_request) Successful in 2m44s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m50s
CI Code / Linux (arch) (pull_request) Successful in 5m25s
CI Code / Linux (debian) (pull_request) Successful in 6m50s
2026-05-05 16:06:54 +03:00
2990ec795a refactor(flatfile): single-line file header with format-version marker
All checks were successful
CI Code / Check spelling (pull_request) Successful in 1m32s
CI Code / Check coding style (pull_request) Successful in 3m14s
CI Code / Code Coverage (pull_request) Successful in 2m44s
CI Code / Linux (debian) (pull_request) Successful in 4m50s
CI Code / Linux (arch) (pull_request) Successful in 5m33s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m51s
2026-05-05 14:56:07 +03:00
b8a4900393 fix(jid): treat NULL/empty/"(null)" barejid the same as resource
Some checks failed
CI Code / Check spelling (pull_request) Successful in 2m0s
CI Code / Check coding style (pull_request) Failing after 2m10s
CI Code / Linux (debian) (pull_request) Successful in 8m29s
CI Code / Linux (ubuntu) (pull_request) Successful in 8m33s
CI Code / Code Coverage (pull_request) Successful in 6m50s
CI Code / Linux (arch) (pull_request) Successful in 10m30s
2026-05-04 18:52:39 +03:00
3daee1216b fix(jid): treat NULL/empty/"(null)" resource as bare jid
Some checks failed
CI Code / Code Coverage (pull_request) Failing after 13m44s
CI Code / Check spelling (pull_request) Failing after 14m13s
CI Code / Check coding style (pull_request) Failing after 14m43s
CI Code / Linux (ubuntu) (pull_request) Failing after 15m16s
CI Code / Linux (debian) (pull_request) Failing after 15m53s
CI Code / Linux (arch) (pull_request) Failing after 16m23s
2026-05-04 18:29:55 +03:00
024e4b2336 style(flatfile): drop vim modeline from log file header 2026-05-04 18:29:55 +03:00
09b2295414 fix(export): open sqlite on demand when flatfile is active backend
Some checks failed
CI Code / Code Coverage (pull_request) Failing after 12m27s
CI Code / Check spelling (pull_request) Failing after 12m56s
CI Code / Check coding style (pull_request) Failing after 13m33s
CI Code / Linux (ubuntu) (pull_request) Failing after 14m3s
CI Code / Linux (debian) (pull_request) Failing after 14m38s
CI Code / Linux (arch) (pull_request) Failing after 15m12s
2026-05-04 17:35:59 +03:00
16aa569329 feat(history): /history backend command and status-bar indicator
Some checks failed
CI Code / Code Coverage (pull_request) Failing after 10m31s
CI Code / Check spelling (pull_request) Failing after 11m2s
CI Code / Check coding style (pull_request) Failing after 11m33s
CI Code / Linux (ubuntu) (pull_request) Failing after 12m2s
CI Code / Linux (debian) (pull_request) Failing after 12m33s
CI Code / Linux (arch) (pull_request) Failing after 13m5s
2026-05-04 17:17:41 +03:00
48b89460b7 feat(flatfile): warn on missing or mismatched log format-version 2026-05-04 17:17:33 +03:00
a9b181beb7 test(bench): add F17 forward-iteration coverage symmetric to F16 2026-05-04 17:16:01 +03:00
52f97f7195 feat(history): autocomplete JID for verify/export/import
Some checks failed
CI Code / Code Coverage (pull_request) Failing after 12m11s
CI Code / Check spelling (pull_request) Failing after 12m14s
CI Code / Check coding style (pull_request) Failing after 12m19s
CI Code / Linux (ubuntu) (pull_request) Failing after 12m25s
CI Code / Linux (debian) (pull_request) Failing after 12m28s
CI Code / Linux (arch) (pull_request) Failing after 12m32s
2026-05-04 15:42:19 +03:00
352eb26f71 refactor(flatfile): drop dead cursor_offset, guard inverted range
Some checks failed
CI Code / Check spelling (pull_request) Successful in 1m25s
CI Code / Check coding style (pull_request) Successful in 2m11s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m47s
CI Code / Linux (debian) (pull_request) Successful in 7m58s
CI Code / Code Coverage (pull_request) Failing after 11m18s
CI Code / Linux (arch) (pull_request) Failing after 11m32s
2026-05-04 15:30:45 +03:00
875f8f4100 fix(flatfile): drop cursor optim + guard n_entries==0
Some checks failed
CI Code / Check spelling (pull_request) Failing after 22s
CI Code / Check coding style (pull_request) Successful in 34s
CI Code / Linux (debian) (pull_request) Successful in 5m23s
CI Code / Linux (arch) (pull_request) Successful in 6m1s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m18s
CI Code / Code Coverage (pull_request) Successful in 7m32s
Cursor was set to oldest of read window, not oldest of returned
batch — each page-up jumped past entire ranges (F16: 600/2000 msgs).
Always bisect now; correct + simpler. Cost: +5-10 ms per page-up at
1M msgs, well below UI threshold.

Also guard state->entries NULL when total_lines>0 && n_entries==0
(every line failed _ff_maybe_index_line). Backup would segfault.

F16 tightened to received == total_msgs. Catches both stuck-on-
entries[0] and cursor-jumping regressions.
2026-05-04 15:09:51 +03:00
a7da9e2add fix(flatfile): page-up cursor must not stick on entries[0]
All checks were successful
CI Code / Check spelling (pull_request) Successful in 23s
CI Code / Check coding style (pull_request) Successful in 37s
CI Code / Code Coverage (pull_request) Successful in 2m39s
CI Code / Linux (debian) (pull_request) Successful in 4m51s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m47s
CI Code / Linux (arch) (pull_request) Successful in 9m59s
When the cursor lands on entries[0].byte_offset, the !from_start
backup logic produces an empty [X, X) range, get_previous_chat
returns DB_RESPONSE_EMPTY, the UI sets WIN_SCROLL_REACHED_TOP and
never tries again — page-up is permanently stuck.

Guard the cursor optimisation with two conditions: index must be
non-empty AND cursor must not equal entries[0].byte_offset. When
either fails, fall through to the end_time bisect path which
correctly locates the byte range with older messages.

F16 regression test in bench_failure_modes:
  without guard: received=200 in 3 iters (premature EMPTY)
  with guard:    received=600 in 7 iters (truly reaches top)
2026-05-04 12:14:54 +03:00
1fe3808b23 style(export): gchar*/g_free consistency + auto_char in read loop
All checks were successful
CI Code / Check spelling (pull_request) Successful in 2m12s
CI Code / Check coding style (pull_request) Successful in 2m23s
CI Code / Linux (arch) (pull_request) Successful in 5m35s
CI Code / Linux (debian) (pull_request) Successful in 6m59s
CI Code / Linux (ubuntu) (pull_request) Successful in 7m41s
CI Code / Code Coverage (pull_request) Successful in 6m59s
2026-05-02 16:18:48 +03:00
555faaa232 perf+harden(export): 25x faster export at 1M, mkstemp, full-body dedup
Some checks failed
CI Code / Check spelling (pull_request) Failing after 21s
CI Code / Linux (arch) (pull_request) Failing after 3m6s
CI Code / Check coding style (pull_request) Failing after 3m10s
CI Code / Code Coverage (pull_request) Successful in 2m49s
CI Code / Linux (debian) (pull_request) Successful in 4m26s
CI Code / Linux (ubuntu) (pull_request) Successful in 4m38s
Address 6 of 7 high/medium audit findings on database_export.c.
End-to-end migration of 1M rows: ~5 min -> ~1 min.

  H1  cache g_slist_length(merged) out of the write loop — was O(n²)
      via 2000 list walks at 500-row progress interval, now O(1).
        S7a_export_cold @ 1M:  309 s -> 11.2 s  (-96 %)

  L1  splice existing flatfile lines into merged via ownership
      transfer instead of 12-strdup deep-copy + g_date_time_ref;
      free list nodes only, NULL out existing.
        S7b_export_dedup @ 1M: 304 s -> 13.3 s  (-96 %)
        RSS @ S7b 1M: 2.4 GB -> 1.9 GB

  H2  mkstemp() with random suffix instead of fixed
      "{log_path}.export.tmp". Eliminates the unlink-and-retry race
      where two concurrent exports could clobber each other's
      in-flight tmp files.

  M4  SHA-256 over full body (incremental g_checksum_update) in
      the dedup-key fallback. Previous body[:256] hash collided
      on common preambles (signatures, code blocks) — second
      occurrence silently dropped on import.

  M2  Refuse _ff_read_all_lines on files > 2 GB. Each line expands
      ~10x in heap on parse; without a cap we OOM-kill prof on
      large flatfiles. Surface a clear error instead.

  M5  Break the outer import loop on system-level INSERT failure.
      Was cascading "Import of X failed" through every remaining
      contact when the real issue was disk-full / sqlite locked.

Verified on bench-pipeline-max: 1M rows, full content diff,
mismatches=0. baseline.csv updated.

Deferred: M3 O_TMPFILE (platform-conditional + linkat dance;
mkstemp covers most of the same window) and M6 SIGINT cancellation
(needs event-loop architecture changes).
2026-04-30 17:10:39 +03:00
b1d820462a feat(bench): synthetic load harness for flat-file backend (P1-P5)
End-to-end performance and correctness harness for the flat-file +
SQLite database backends. Lives in tests/bench/, built only on
demand (`make bench`); not part of `make check`.

Components

  gen_history (P1)
    Deterministic corpus generator. Knobs: lines, contacts, years,
    seed, stanza-id mode (uuid/libpurple/conversations/mixed), LMC
    rate, MAM-OOO rate, resources/contact, length profile
    (short/mixed/long/extreme). Emits the canonical
    flatlog/<account>/<contact>/history.log layout used by
    ff_verify_integrity. ~340 LOC.

  bench_runner (P1, P2.5)
    S1 cold tail-access via sparse index
    S2 warm tail-access (page cache hot)
    S3 deep pagination (1000 binary-search lookups)
    S4 first-time index build (cold file -> ff_state_ensure_fresh)
    S5 incremental extend (asserts no full rebuild path)
    S6 real ff_verify_integrity over the contact tree
    Reports total/err/warn/info issue counts in the CSV note.

  bench_long_messages (P2)
    L1-L14 long-message stress: 1KB up to 9.9MB bodies, plus
    oversized line rejection (10MB+1 -> ff_readline returns ""),
    embedded-newline / pipe / emoji body patterns, full parse on
    100x1MB, 1000x100KB sustained append.

  bench_failure_modes (P3)
    F1-F15 failure-injection: truncated last line, mid-file CRLF,
    mid-file BOM, LMC cycle, LMC depth>FF_MAX_LMC_DEPTH, manual
    ': ' in resource, RTL/ZWSP, Latin-1 byte, empty body,
    mtime/inode flip, empty file. Each test asserts expected issue
    levels and reports PASS/FAIL.

  bench_export_import (P5)
    Links real database_export.c + database_sqlite.c + database.c
    and drives log_database_export_to_flatfile /
    log_database_import_from_flatfile under load.
    Subcommands: seed, export, import, roundtrip, verify.
    S7a/b export, S8a/b import, S8e roundtrip with full byte-by-byte
    content diff of every row in (from_jid, to_jid, message,
    timestamp, type, stanza_id, archive_id, encryption, replace_id).

Make targets

  bench-quick / bench / bench-full
  bench-longmsg, bench-failure
  bench-multicontact, bench-lmc, bench-ooo
  bench-export, bench-import, bench-roundtrip
  bench-pipeline, bench-pipeline-max (1M rows)
  bench-compare, bench-update-baseline

Volume controls: BENCH_VOLUME (small/medium/max), BENCH_PIPE_ROWS,
BENCH_PIPE_ROWS_MAX, BENCH_DATA_DIR, BENCH_CSV.

Baseline + regression checking (P4)

  tests/bench/baseline.csv       51 rows: S1-S6 x {small,lmc,ooo}
                                 + L1-L14 + F1-F15 (11 of 15) +
                                 S7/S8 x {pipe100k, pipe1M}.
  compare_baseline.py            median over duplicate rows;
                                 exits 1 on any (scenario, volume)
                                 slowdown >= threshold (default 25%).

Verified at scale

  bench-pipeline-max: 1,000,000 rows, full content diff
    seed   17 s, export 304 s, import 31 s, idempotent re-import 10 s,
    diff 2.7 s -- mismatches=0.

Findings surfaced by the harness

  Export scales super-linearly: 4 s @ 100k -> 304 s @ 1M (76x for
    10x rows). Cause: g_slist_sort on the merged list + per-row
    ProfMessage/ff_parsed_line_t allocations. RSS peaks at 1.4 GB
    on 1M. Worth a follow-up.
  Export progress reporting only fires during the write phase --
    the merge+sort phase (~95% of wall time at 1M) is silent.
  /history export and /history import are blocking on the main
    UI thread; profanity is frozen for the duration (~5 min @ 1M).
  ff_readline sets *truncated=TRUE on partial-write tail but
    ff_verify_integrity does not surface this -- partial writes
    go unflagged (failure-injection F1).
  Parser silently truncates body at first unescaped ': ' if a
    resource was manually edited to contain it (F9).
  In gen_history (caught by the bench's own real-verify pass):
    g_strndup mid-codepoint truncation on UTF-8 bank strings -- fixed.

Linkage strategy

  database_flatfile.c + parser + verify + common.c are linked
  unconditionally. The export/import bench additionally links
  database.c + database_sqlite.c + database_export.c. bench_stubs.c
  provides minimal stubs for log_*, prefs_*, connection_get_jid,
  jid_create, files_*, message_*, ui hooks. integrity_issue_free
  is a weak symbol so it falls back to the real database.c
  implementation when that file is linked.
2026-04-30 15:39:30 +03:00
8868d58920 fix(review): address PR #94 review #37/#38 reviewer comments
Parser (src/database_flatfile_parser.c):
- ff_jid_to_dir: drop GString+step1, in-place mutate str_replace result
- ff_ensure_dir: g_file_test(G_FILE_TEST_IS_SYMLINK) instead of g_lstat
- ff_unescape_*: early-continue for non-escape branch (less nesting)
- ff_readline: strdup("") instead of malloc(1)+'\0' for skip-line return
- ff_write_line: auto_char safe_msg + single fprintf (drop GString full_line)
- ff_parse_line: early return on missing closing bracket;
  extract metadata-parts loop into _ff_parse_meta_parts helper

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

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

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

Tests:
- 6 new invalid-date parser tests: empty / partial ISO / garbage /
  impossible calendar / negative year / far future timestamp
- New functional test message_db_history_multi_resource verifying
  same-JID-different-resource history rehydration preserves resources
- test_database_stress.c LMC chain loop: auto_char buf, drop redundant
  comment/empty pre-filter (ff_parse_line handles those)
2026-04-30 10:53:03 +03:00
7ba7e53291 refactor: extract helpers for verify print, scan loop, and per-contact export
- extract _show_integrity_issues() from cmd_history verify block
- extract _ff_scan_lines() shared by _ff_state_build and _ff_state_extend
- extract _export_one_contact() from log_database_export_to_flatfile loop
2026-03-28 12:58:27 +03:00
a96a4ad41e fix(review): address PR #94 reviewer comments
Blockers:
- SPDX/CProof copyright headers (7 files)
- stanza-id non-uniqueness: warn-only, don't skip (matches SQLite)
- fopen → open(O_EXCL|O_NOFOLLOW, 0600) + fdopen in export

Medium:
- log_info → log_warning for SQLite fallback
- remove prefs_save() auto-save from backend switch
- vtable null → log_warning in 4 dispatch functions
- log_debug → log_warning + null error handling in export
- rename failure → cons_show_error to user
- remove unused total_skipped variable
- LMC respects PREF_CORRECTION_ALLOW

Low nits:
- man page .SS fix + jabber.space URL
- profrc.example simplified
- show flatfile path on /logging flatfile
- EXPORT_PROGRESS_INTERVAL constant (magic 500)
- FF_META_PREFIX_ID/AID constants + FF_INDEX_STEP comment
- buffer overflow guard on close[1]/close[2]
- for-loop → memchr for JID resource scan
- BOM check extracted to ff_skip_bom() (DRY)
- index building extracted to _ff_maybe_index_line()
- O_APPEND+O_EXCL comment rewritten
- Makefile.am spaces → tabs
2026-03-28 12:38:39 +03:00
58002409ff test(functests): migration/MUC/timestamp tests, group rebalancing
All checks were successful
CI Code / Check spelling (pull_request) Successful in 24s
CI Code / Check coding style (pull_request) Successful in 41s
CI Code / Linux (ubuntu) (pull_request) Successful in 6m56s
CI Code / Linux (debian) (pull_request) Successful in 7m0s
CI Code / Code Coverage (pull_request) Successful in 7m5s
CI Code / Linux (arch) (pull_request) Successful in 9m29s
Add 11 new export/import tests (24 → 35): 4 MUC database, 3 timestamp
verification, 3 migration stress, 1 LMC correction with negative assert.

Add delay-stamp timestamp checks to 5 base tests via /time chat set
iso8601 and /history on (previously passed by accident from PTY buffer).

Replace magic numbers: NEGATIVE_ASSERT_TIMEOUT, MIN_ELAPSED_INIT.
Add per-test slow/fast thresholds and TZ override (prof_test_tz).

Rebalance groups by whole logical blocks (spread 46s → 3s):
G1=52s G2=50s G3=53s G4=53s. All 129 tests pass.
2026-03-19 19:05:36 +03:00
2b1b70665b fix(functests): fix send_receipt_request 60s timeout, rebalance groups, add per-test timing
All checks were successful
CI Code / Check spelling (pull_request) Successful in 19s
CI Code / Check coding style (pull_request) Successful in 35s
CI Code / Code Coverage (pull_request) Successful in 2m28s
CI Code / Linux (debian) (pull_request) Successful in 4m23s
CI Code / Linux (arch) (pull_request) Successful in 4m41s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m14s
Fix send_receipt_request test that wasted 60s on every run:
- Change caps hash from sha-256 to sha-1 (profanity only supports sha-1
  for standard caps path, sha-256 goes to unsupported-hash fallback)
- Fix expected output: 'Buddy1 is online' → 'Buddy1 (laptop) is online'
  (resource is always shown for full JID presence)
- Wrap prof_output_exact in assert_true so mismatches fail fast instead
  of silently waiting the full expect_timeout

Rebalance test groups for parallel execution:
- Move Disco tests (14) from Group 3 → Group 1
- Move DB History tests (12) from Group 2 → Group 3
- Before: G1=31s G2=63s G3=30s G4=37s (bottleneck 63s)
- After:  G1=46s G2=38s G3=40s G4=36s (bottleneck 46s, -27%)

Add per-test wall-clock timing via clock_gettime(CLOCK_MONOTONIC)
in init_prof_test/close_prof_test for profiling individual tests.
2026-03-12 21:02:22 +03:00
0d38a0a2cb test: add database stress tests (14 tests, rapid writes, large messages, LMC chains, dedup)
All checks were successful
CI Code / Check spelling (pull_request) Successful in 17s
CI Code / Check coding style (pull_request) Successful in 31s
CI Code / Code Coverage (pull_request) Successful in 4m1s
CI Code / Linux (debian) (pull_request) Successful in 5m38s
CI Code / Linux (ubuntu) (pull_request) Successful in 5m39s
CI Code / Linux (arch) (pull_request) Successful in 6m7s
2026-03-12 18:58:52 +03:00
71f9ab5007 perf: O(1) dedup/LMC cache, g_slist_append → prepend+reverse
- Add archive_ids hash set and stanza_senders hash map to
  ff_contact_state_t, populated during index build/extend via
  lightweight _ff_cache_line_ids() (no full parse overhead).
- Replace O(n) file scans in _ff_has_archive_id() and
  _ff_find_original_sender() with O(1) hash table lookups.
- Replace g_slist_append with g_slist_prepend + g_slist_reverse
  in _ff_apply_lmc, _flatfile_get_previous_chat, and
  ff_verify_integrity to avoid O(n²) list building.
- Add db_sqlite_last_changes() declaration to database.h.
2026-03-12 18:52:00 +03:00
06463b75b4 style: apply clang-format and const qualifiers from master 2026-03-12 18:52:00 +03:00
507ef91b5f feat: complete field parity, harden export/import, add tests
Export/Import improvements:
- Replace pagination with direct SQL query (db_sqlite_get_all_chat)
- Wrap import in SQL transaction with rollback on error
- Add fsync before fclose in export for data safety
- Sort merged output by timestamp with secondary key (stanza_id, from_jid)
- Export archive_id and marked_read from SQLite (lossless migration)
- Add progress indication every 500 messages during write/import
- Expand dedup key body prefix from 64 to 256 chars
- Fix g_slist_append O(n²) → g_slist_prepend + g_slist_reverse O(n)

Field parity (to_jid, to_resource, marked_read):
- Add fields to ff_parsed_line_t struct
- Write/parse to:|to_res:|read: metadata tags in flatfile format
- Pass to_resource through _ff_add_message and all callers
- Add marked_read to ProfMessage struct with -1 default (unset)
- Preserve fields across export/import round-trips

Tests (19 new: 11 unit + 8 functional):
- Unit: to_jid_and_marked_read, bracket_in_stanza_id, backslash_in_resource,
  mucpm_type, all_enc_types, crlf_handling, to_jid_special_chars,
  multiple_lines, parsed_line_free_null_safe, no_space_rejected,
  unclosed_bracket
- Functional: export_idempotent_no_duplicates, export_lmc_correction_survives,
  switch_preserves_old_backend_data, export_all_contacts,
  import_double_dedup, verify_after_export,
  switch_backends_independent_messages, export_empty_contact
- Rebalance test groups: move Chat Session from Group 3 to Group 4
  (25/33/30/27 instead of 25/33/36/21)
- Remove hardcoded test counts from group comments

Man page:
- Document /history switch sqlite|flatfile
2026-03-12 18:46:42 +03:00
9f3020e40a Add functional and unit tests for DB export/import
- 2 functional tests: export_sqlite_to_flatfile, import_flatfile_to_sqlite
  with XEP-0203 delay timestamps for deterministic verification
- 27 unit tests for database_export covering edge cases
- Merge test/db-functional-tests: 12 DB persistence tests (test_history)
- Group 2 rebalanced: 25 tests (23 base + 2 SQLite-only export/import)
- #ifdef HAVE_SQLITE guards for export/import tests
- prof_stop() helper in proftest.c
- Makefile.am: source ordering cleanup, new test files added
2026-03-12 18:46:42 +03:00
7e320ccdb0 tests: add functional tests for DB message persistence, rebalance groups
Add backend-agnostic functional tests for database message persistence.
All tests work with both SQLite and flat-file backends.

New tests in test_history.c:
- message_db_history_on_reopen: basic incoming write+read round-trip
- message_db_history_multiple: 3 messages from different resources
- message_db_history_contact_isolation: buddy1 msg absent from buddy2
- message_db_history_special_chars: XML entities survive decode+DB
- message_db_history_outgoing: sent message persists across reopen
- message_db_history_dialog: outgoing + incoming both in history
- message_db_history_empty: no crash on contact with no history
- message_db_history_long_message: 1000+ char body not truncated
- message_db_history_newline: embedded LF stored correctly
- message_db_history_service_chars: backslash pipe percent braces etc
- message_db_history_verify: /history verify (ifdef HAVE_HISTORY_VERIFY)
- message_db_history_lmc: XEP-0308 correction replaces original in DB

Rebalance test groups for parallel execution (19/22/22/21):
- Group 1: Connect, Ping, Rooms, Software, LastActivity
- Group 2: Message, Receipts, Roster, DB History
- Group 3: Chat Session, Presence, Disconnect
- Group 4: MUC, Carbons
2026-03-12 18:46:42 +03:00
b5fe714a89 feat: add /history switch for runtime database backend switching
Add log_database_switch_backend() that closes the current backend,
updates PREF_DBLOG preference, and reinitializes with the new backend
without requiring a reconnect or restart.

New command: /history switch sqlite|flatfile
- Validates connection state and backend name
- Skips if already using the requested backend
- Autocomplete support for the subcommand
2026-03-12 18:46:42 +03:00
1f3d3117bf feat(draft): add /history export|import for SQLite<->flatfile migration
DRAFT — not yet tested end-to-end with a live XMPP session.

New commands:
  /history export [<jid>]  — copy messages from SQLite to flat-file
  /history import [<jid>]  — copy messages from flat-file to SQLite
Both merge with existing data; duplicates are skipped using stanza-id
or a SHA-256 fallback key (timestamp + sender + body prefix).

Implementation (src/database_export.c):
- Export: paginate SQLite via get_previous_chat, read existing flatfile
  for dedup, write merged result via ff_write_line + atomic rename
- Import: parse flatfile lines via ff_parse_line, build dedup set from
  SQLite, insert new messages via add_incoming (preserves original
  timestamps for both directions)
- List all contacts: db_sqlite_list_contacts() queries UNION of
  DISTINCT from_jid/to_jid; flatfile enumerates flatlog directories

Wiring:
- database.h: declare export/import functions + db_sqlite_list_contacts
- database_sqlite.c: add db_sqlite_list_contacts()
- cmd_defs.c: add export/import to /history synopsis and args
- cmd_funcs.c: add export/import handlers in cmd_history()
- cmd_ac.c: add 'export' and 'import' to history_ac
- Makefile.am: add database_export.c to core_sources
- stub_database.c: add stubs for test linking
- profanity.1: document export/import in man page

All code guarded with #ifdef HAVE_SQLITE — builds cleanly without it.
2026-03-12 18:46:42 +03:00
23723376c6 fix: harden flatfile backend, make SQLite optional
Build system:
- Add --without-sqlite configure flag (AM_CONDITIONAL BUILD_SQLITE)
- Guard database_sqlite.c with HAVE_SQLITE in Makefile.am, database.c,
  database.h and stub_database.c
- Fall back to flatfile when SQLite not compiled in

Security (database_flatfile.c):
- MAM dedup: skip incoming messages with duplicate stanza-id (archive_id)
- LMC sender validation: reject corrections from mismatched JIDs
- Add flock() advisory locking to prevent interleaved writes from
  concurrent instances
- Check fprintf return when writing file header

Autocomplete (cmd_ac.c):
- Add 'flatfile' to /privacy logging autocomplete
- Add dedicated /history autocomplete with on/off/verify (was boolean-only)

Code quality:
- Fix fwrite return type: size_t not ssize_t (database_flatfile_parser.c)
- Fix mixed allocators in ff_readline: use malloc() consistently for
  overlength-line fallback instead of g_strdup()
- Fix index alignment in _ff_state_extend: use local counter so index
  step doesn't depend on total_lines from initial build

Documentation:
- Fix page: correct directory structure (history.log not per-day files)
- Fix page: add aid:{archive_id} to line format example
- Fix page: missing newline before .SH BUGS
2026-03-12 18:46:42 +03:00
16ae7fb85c database_flatfile: single-file storage with sparse index
Replace per-contact directory structure with a single flat file per contact.
Each file uses pipe-delimited records with a sparse byte-offset index
that is rebuilt on demand and cached in memory.

- Rewrite _ff_write_record / _ff_read_records for single-file format
- Add sparse index (every Nth record) for O(log N) seek on history read
- Cursor-based pagination: _ff_get_previous_chat uses saved byte offset
- LMC corrections applied in-place during read (_ff_apply_lmc)
- Newline escaping (\n) in message bodies for line-based storage
- Simplify verify: parse + timestamp order + LMC reference check
- Update parser helpers for new field layout
2026-03-12 18:46:42 +03:00
d25f90899c refactor: split database_flatfile.c into functional modules
Split the monolithic database_flatfile.c into:
- database_flatfile.h: internal header with shared types and prototypes
- database_flatfile_parser.c: parsing, escaping, IO helpers
- database_flatfile_verify.c: integrity verification logic
- database_flatfile.c: core backend (init, write, read, LMC, vtable)
2026-03-12 18:46:42 +03:00
89b90e5cf2 fix(flatfile): harden flat-file backend against injection and traversal attacks
Security fixes for 7 vulnerabilities in database_flatfile.c:

1. Path traversal via crafted JID (HIGH):
   _ff_jid_to_dir() now strips '/', '\' -> '_' and collapses '..' -> '__',
   preventing a malicious federated JID from escaping the log directory.

2. Log injection via unescaped message body (HIGH):
   Add _ff_escape_message()/_ff_unescape_message() -- escape \n, \r, \\
   in message text on write, unescape on read. Prevents remote contacts
   from injecting fake log lines with forged sender/timestamp/encryption.

3. Metadata field injection (HIGH):
   Add _ff_escape_meta_value()/_ff_unescape_meta_value() -- escape |, ],
   \\, \n, \r in stanza_id/archive_id/replace_id. Parser uses escape-aware
   _ff_find_unescaped_char() and _ff_split_meta() instead of strchr/strsplit.

4. Sender ": " parsing confusion (MEDIUM):
   Escape ": " -> "\: " in XMPP resource on write. Parser uses
   _ff_find_unescaped_colonspace() + _ff_unescape_sender_resource().

5. LMC correction chain cycle -> infinite loop (MEDIUM):
   Add visited hash-set and FF_MAX_LMC_DEPTH (100) limit to chain walker.

6. Unbounded getline() -> OOM (MEDIUM):
   Add FF_MAX_LINE_LEN (10 MB) cap in _ff_readline(); overlength lines
   are skipped with a warning. Replaces all char buf[8192]/fgets() sites
   with dynamic getline() + truncated-line detection at EOF.

7. Symlink attack + TOCTOU on file creation (MEDIUM):
   Replace fopen("a") with open(O_WRONLY|O_APPEND|O_CREAT|O_EXCL|O_NOFOLLOW)
   + fdopen() -- atomic new-file detection, symlink rejection via O_NOFOLLOW.
   _ff_ensure_dir() checks g_lstat() for symlinks. File permissions 0600
   set via open() mode, not post-hoc g_chmod().
2026-03-12 18:46:42 +03:00
31e8c30c34 feat: add flat-file database backend for message history
Add pluggable storage backend abstraction (vtable) to the database layer,
allowing selection between SQLite (default) and a new flat-file backend
that stores messages as human-readable plain text files.

New files:
- database_sqlite.c: extracted SQLite backend from database.c
- database_flatfile.c: plain text backend with tolerant parser,
  LMC correction chains, UTF-8/BOM/CRLF handling, integrity checks

Commands:
- /privacy logging flatfile — switch to flat-file backend
- /history verify [<jid>] — check integrity of stored history

Fixes:
- Add missing 'off' guard in flatfile backend
- Enable CHLOG/HISTORY prefs when switching to flatfile mode

Logs stored in ~/.local/share/profanity/flatlog/{account}/{contact}/{date}.log
Format: {ISO8601} [{type}|{enc}|id:{id}|corrects:{id}] {sender}: {message}

Updated: CHANGELOG, CONTRIBUTING.md, profrc.example, man page, cmd_defs,
Makefile.am, test stubs, functional test support (PROF_FLATFILE=1)
2026-03-12 18:46:42 +03:00
15 changed files with 313 additions and 542 deletions

View File

@@ -204,13 +204,6 @@ 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,11 +396,12 @@ 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 -Wpointer-arith"
AM_CFLAGS="$AM_CFLAGS -Wnull-dereference -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,9 +310,11 @@ _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);
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);
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;
_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_printf("%s↔%s/chatlog.db",
from_a ? from_a : "?", to_a ? to_a : "?");
issue->file = g_strdup("chatlog.db");
issue->line = id_a;
issue->message = g_strdup_printf("Timestamp out of order: row %d (%s) > row %d (%s)",
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 : "?",
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_printf("%s↔%s/chatlog.db",
from ? from : "?", to ? to : "?");
issue->file = g_strdup("chatlog.db");
issue->line = id;
issue->message = g_strdup_printf("Broken LMC reference: row %d references non-existent row %d",
issue->message = g_strdup_printf("Broken LMC reference in %s↔%s: row %d references non-existent row %d",
from ? from : "?", to ? to : "?",
id, replaces_id);
issues = g_slist_append(issues, issue);
}

View File

@@ -70,12 +70,9 @@ 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,7 +24,8 @@ 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,25 +105,26 @@ 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.
@@ -159,15 +160,14 @@ 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,8 +180,7 @@ 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();
@@ -210,14 +209,10 @@ 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",
@@ -314,29 +309,21 @@ 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;
}
@@ -345,16 +332,14 @@ 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;
@@ -379,10 +364,8 @@ 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;
}
@@ -416,23 +399,15 @@ 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);
@@ -448,14 +423,10 @@ 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;
@@ -490,14 +461,10 @@ 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;
@@ -540,36 +507,24 @@ 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);
@@ -584,13 +539,11 @@ 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;
@@ -599,8 +552,7 @@ 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);
@@ -613,8 +565,7 @@ 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;
@@ -632,7 +583,8 @@ 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);
}
@@ -662,19 +614,13 @@ 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 (!a || !b) {
usage();
return 2;
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; }
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",
@@ -685,23 +631,15 @@ 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,8 +109,7 @@ 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);
}
@@ -125,18 +124,15 @@ 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++;
@@ -152,10 +148,7 @@ 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;
@@ -189,8 +182,7 @@ 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.
@@ -235,8 +227,7 @@ 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");
@@ -271,17 +262,14 @@ 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");
@@ -308,8 +296,7 @@ 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
@@ -350,8 +337,7 @@ 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");
@@ -390,8 +376,7 @@ 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");
@@ -427,8 +412,7 @@ 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");
@@ -466,8 +450,7 @@ 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");
@@ -505,8 +488,7 @@ 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");
@@ -536,8 +518,7 @@ 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");
@@ -582,8 +563,7 @@ 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);
@@ -620,8 +600,7 @@ 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
@@ -736,8 +715,7 @@ 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;
@@ -834,12 +812,9 @@ 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,12 +68,9 @@ 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;
@@ -98,7 +95,8 @@ 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
@@ -157,8 +155,7 @@ 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);
@@ -170,8 +167,7 @@ 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);
@@ -180,7 +176,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,
@@ -204,8 +200,7 @@ 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;
@@ -229,12 +224,9 @@ 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;
}
@@ -265,8 +257,7 @@ 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",
@@ -346,11 +337,8 @@ 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;
@@ -385,8 +373,7 @@ 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++) {
@@ -394,11 +381,7 @@ 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",
@@ -430,17 +413,12 @@ 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);
@@ -448,8 +426,7 @@ 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,16 +77,11 @@ 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;
@@ -126,8 +121,7 @@ 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) {
@@ -197,8 +191,7 @@ 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;
}
@@ -236,8 +229,7 @@ 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);
@@ -305,10 +297,8 @@ 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;
}
@@ -340,8 +330,7 @@ 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++) {
@@ -370,8 +359,7 @@ 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,
@@ -392,8 +380,7 @@ _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;
}
@@ -419,8 +406,7 @@ 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 : "");
@@ -433,16 +419,11 @@ 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;
}
@@ -464,8 +445,7 @@ 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;
}
@@ -474,8 +454,7 @@ 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,50 +100,14 @@ 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,14 +57,16 @@
// ---------------------------------------------------------------------------
// 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,
@@ -107,44 +109,20 @@ 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;
}
@@ -226,26 +204,8 @@ 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]);
@@ -305,35 +265,24 @@ 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*
@@ -392,15 +341,13 @@ 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];
@@ -411,8 +358,7 @@ 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);
@@ -433,12 +379,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,10 +61,7 @@
#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,10 +439,7 @@ 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) {
@@ -465,7 +462,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;
@@ -486,14 +483,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);
}
@@ -503,24 +500,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;
}
@@ -529,34 +526,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);
@@ -566,21 +563,23 @@ 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);
@@ -590,7 +589,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");
@@ -598,14 +597,15 @@ 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,5 +625,6 @@ 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