// SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of CProof. // See LICENSE for the full GPLv3 text and the special OpenSSL linking exception. /* * bench_failure_modes.c * vim: expandtab:ts=4:sts=4:sw=4 * * F1–F17 failure-injection tests. Each test crafts a deliberately corrupt * or pathological flat-file and asserts how the backend handles it: * * F1 Truncated last line (crash mid-fwrite simulation) * F2 CRLF line endings mid-corpus * F3 BOM not at start (mid-file) * F7 LMC cycle A→B→A (read path must not loop) * F8 LMC chain longer than FF_MAX_LMC_DEPTH (graceful truncation) * F9 Resource containing literal ": " (parser must reject the bad line) * F10 RTL / zero-width chars in resource (parse OK, preserved literally) * F11 Latin-1 fragment in UTF-8 file (invalid UTF-8 → fallback or ERROR) * F12 Empty body — receipt-only carbon equivalent * F14 mtime/inode flip — file replaced under us, ensure_fresh must rebuild * F15 Empty file (0 bytes) — verify reports INFO and continues * F16 Page-Up cursor must not stick on entries[0] (regression guard) * F17 Forward iteration via start_time covers every message (no gaps) * * Each test prints PASS/FAIL with detail and writes a CSV row: * F#, "failure", file_size, line_count, wall_ms, peak_rss_kb, note * * Tests run independently against per-test tmp directories; no shared state. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "bench_common.h" #include "bench_csv.h" #include "config/account.h" #include "database_flatfile.h" #define ACCOUNT_JID "fail@bench.example" #define CONTACT_JID "peer@bench.example" // --------------------------------------------------------------------------- // Test framework typedef struct { const char* tmp_dir; const char* csv_path; const char* tests; // comma list or "all" int passed; int failed; } fail_ctx_t; static int test_enabled(const char* list, const char* name) { if (!list || g_strcmp0(list, "all") == 0) return 1; auto_gcharv gchar** parts = g_strsplit(list, ",", -1); for (int i = 0; parts[i]; i++) { if (g_strcmp0(g_strstrip(parts[i]), name) == 0) return 1; } return 0; } // Set BENCH_DATA_DIR to and return path to history.log inside the // canonical layout; create directories as needed. static char* setup_test_dir(const char* tmp_root, const char* test_name) { auto_gchar gchar* test_root = g_strdup_printf("%s/%s", tmp_root, test_name); g_mkdir_with_parents(test_root, 0755); setenv("BENCH_DATA_DIR", test_root, 1); auto_gchar gchar* account_dir = ff_jid_to_dir(ACCOUNT_JID); auto_gchar gchar* contact_dir = ff_jid_to_dir(CONTACT_JID); auto_gchar gchar* full = g_strdup_printf("%s/flatlog/%s/%s", test_root, account_dir, contact_dir); g_mkdir_with_parents(full, 0755); return g_strdup_printf("%s/history.log", full); } typedef struct { int total; int errors; int warnings; int infos; char* first_err; char* first_warn; } verify_summary_t; static void verify_summary_free(verify_summary_t* s) { if (!s) return; g_free(s->first_err); g_free(s->first_warn); } static void run_verify(verify_summary_t* out) { g_free(g_flatfile_account_jid); g_flatfile_account_jid = g_strdup(ACCOUNT_JID); GSList* issues = ff_verify_integrity(CONTACT_JID); memset(out, 0, sizeof(*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; switch (i->level) { case INTEGRITY_ERROR: out->errors++; 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 : ""); break; case INTEGRITY_INFO: out->infos++; break; } } g_slist_free_full(issues, (GDestroyNotify)integrity_issue_free); } static void report(fail_ctx_t* ctx, const char* tag, gboolean ok, double wall_ms, const char* path, const char* note) { fprintf(stderr, " %s %-3s %.1fms %s\n", ok ? "PASS" : "FAIL", tag, wall_ms, note ? note : ""); 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; bench_csv_append(ctx->csv_path, tag, ok ? "fail-pass" : "fail-FAIL", sz, 0, wall_ms, bench_peak_rss_kb(), note ? note : ""); } } // --------------------------------------------------------------------------- // Helpers to build correct lines static void write_normal_line(FILE* fp, int seq, const char* body) { GDateTime* base = g_date_time_new_utc(2025, 6, 15, 12, 0, 0.0); GDateTime* t = g_date_time_add_seconds(base, seq); auto_gchar gchar* iso = g_date_time_format_iso8601(t); g_date_time_unref(t); g_date_time_unref(base); auto_gchar gchar* sid = g_strdup_printf("F-msg-%d", seq); ff_write_line(fp, iso, "chat", "none", sid, NULL, NULL, "peer@bench.example", "phone", NULL, NULL, -1, body); } // --------------------------------------------------------------------------- // F1 — truncated last line static void test_F1(fail_ctx_t* ctx) { 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. FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); for (int i = 0; i < 10; i++) write_normal_line(fp, i, "ok"); fflush(fp); long sz_before = ftell(fp); fclose(fp); // Truncate one byte (drops the trailing \n) if (truncate(path, sz_before - 1) != 0) { report(ctx, "F1", FALSE, 0, path, "truncate failed"); return; } // Now also append a partial line (simulates crash mid-fwrite). fp = fopen(path, "a"); fprintf(fp, "2025-06-15T12:00:11Z [chat|none|id:partial] peer@bench.example/phone: half-writ"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Expectation: file is parseable (last line is "half-writ" — likely // unparsable depending on whether the timestamp+meta+sender all fit). // We assert verify completes without crashing AND surfaces the partial // line as either a parse-error or merely no-error (depending on // ff_readline truncated detection — currently the warning is not raised). auto_gchar gchar* note = g_strdup_printf("issues total=%d err=%d warn=%d (partial-line tail; expect graceful handle)", s.total, s.errors, s.warnings); report(ctx, "F1", TRUE, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F2 — CRLF mid-corpus static void test_F2(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F2")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F2"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); for (int i = 0; i < 10; i++) { write_normal_line(fp, i, "lf line"); } // Append three CRLF-terminated lines manually. for (int i = 10; i < 13; i++) { fprintf(fp, "2025-06-15T12:%02d:00Z [chat|none|id:crlf-%d] peer@bench.example/phone: crlf line\r\n", i, i); } fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; gboolean ok = s.warnings >= 1; // expect ≥1 CRLF/perms warning auto_gchar gchar* note = g_strdup_printf( "warnings=%d (expect CRLF warning) first_warn=%s", s.warnings, s.first_warn ? s.first_warn : "(none)"); report(ctx, "F2", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F3 — BOM not at start (mid-file) static void test_F3(fail_ctx_t* ctx) { 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); 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"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Expectation: the BOM bytes appear at the start of a line that the // parser doesn't recognise, so we get an unparsable-line ERROR for one row. gboolean ok = s.errors >= 1; auto_gchar gchar* note = g_strdup_printf( "errors=%d warnings=%d (expect 1 unparsable line for mid-file BOM) first=%s", s.errors, s.warnings, s.first_err ? s.first_err : "(none)"); report(ctx, "F3", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F7 — LMC cycle A→B→A (must not loop on read; verify must not crash) static void test_F7(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F7")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F7"); // A: stanza_id="A", no replace // B: stanza_id="B", replaces "A" // A2: stanza_id="A2", replaces "B" — chain ok // CYCLE: stanza_id="C1", replaces "C2" // stanza_id="C2", replaces "C1" (cycle in references) FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); fprintf(fp, "2025-06-15T12:00:00Z [chat|none|id:A] peer@bench.example/phone: original A\n"); fprintf(fp, "2025-06-15T12:00:01Z [chat|none|id:B|corrects:A] peer@bench.example/phone: correction B\n"); fprintf(fp, "2025-06-15T12:00:02Z [chat|none|id:A2|corrects:B] peer@bench.example/phone: correction A2\n"); fprintf(fp, "2025-06-15T12:00:03Z [chat|none|id:C1|corrects:C2] peer@bench.example/phone: cycle leg 1\n"); fprintf(fp, "2025-06-15T12:00:04Z [chat|none|id:C2|corrects:C1] peer@bench.example/phone: cycle leg 2\n"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Expectation: verify completes without infinite loop. Both C1→C2 and // C2→C1 references resolve (each id is present), so no broken-refs. // The actual cycle-walk happens at *read* time, not in verify. gboolean ok = (s.total < 1000); // sanity: must finish fast and not explode auto_gchar gchar* note = g_strdup_printf( "issues total=%d err=%d (cycle handled at read-time, not verify)", s.total, s.errors); report(ctx, "F7", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F8 — LMC chain depth 200 (over FF_MAX_LMC_DEPTH=100) static void test_F8(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F8")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F8"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); // Original message id=A0, then 200 corrections each pointing to the previous. fprintf(fp, "2025-06-15T12:00:00Z [chat|none|id:A0] peer@bench.example/phone: original\n"); for (int i = 1; i <= 200; i++) { fprintf(fp, "2025-06-15T12:%02d:%02dZ [chat|none|id:A%d|corrects:A%d] " "peer@bench.example/phone: correction-%d\n", i / 60, i % 60, i, i - 1, i); } fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // All references resolve (each correction's predecessor is present), so // verify reports no broken refs. The depth-truncation enforced by // FF_MAX_LMC_DEPTH only manifests at read time. Just assert no errors. gboolean ok = (s.errors == 0); auto_gchar gchar* note = g_strdup_printf( "201 lines (1 orig + 200 corrections) errors=%d warns=%d", s.errors, s.warnings); report(ctx, "F8", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F9 — Resource contains literal ": " (manual edit; parser must reject) static void test_F9(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F9")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F9"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); write_normal_line(fp, 0, "ok"); // Manually crafted bad line: resource has unescaped ": ", parser will // split message at the wrong colon and produce garbage. fprintf(fp, "2025-06-15T12:00:01Z [chat|none|id:bad] peer@bench.example/phone: with: colon and: spaces: inside\n"); write_normal_line(fp, 2, "ok2"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Parser splits at first unescaped ": " — message body becomes // "with" instead of the full text. That's not an error from the parser's // POV (it parsed something), so verify reports no issues. We just check // that we didn't crash and that the file as-a-whole is parseable. gboolean ok = (s.errors == 0); auto_gchar gchar* note = g_strdup_printf( "errors=%d (parser splits at first ': ' — not flagged but body truncated)", s.errors); report(ctx, "F9", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F10 — RTL / zero-width chars in resource static void test_F10(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F10")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F10"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); // resource = "phone" + U+202E (RTL override) + U+200B (zero-width space). // We emit the bytes via fwrite to keep the C source ASCII-clean // (-Werror=bidi-chars trips on literals containing RTL controls). static const unsigned char rtl_zwsp[] = { 0xE2, 0x80, 0xAE, 0xE2, 0x80, 0x8B }; fputs("2025-06-15T12:00:00Z [chat|none|id:rtl] peer@bench.example/phone", fp); fwrite(rtl_zwsp, 1, sizeof(rtl_zwsp), fp); fputs(": payload\n", fp); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Should parse fine (valid UTF-8, just unusual). Verify might or might // not flag the control-char check; let's just ensure no crash and total // <= 5 (perms warning + maybe control-char if parser-level). gboolean ok = (s.errors == 0); auto_gchar gchar* note = g_strdup_printf( "errors=%d warnings=%d (RTL/ZWSP preserved literally)", s.errors, s.warnings); report(ctx, "F10", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F11 — Latin-1 fragment (bytes 0xA0+ that aren't valid UTF-8) static void test_F11(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F11")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F11"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); write_normal_line(fp, 0, "before"); // 0xC9 (É in Latin-1) followed by 'o' is invalid UTF-8 (0xC9 is a 2-byte // lead expecting a continuation byte, but 'o' isn't one). Emit the prefix // and the bad byte separately to keep the C string literal valid. fputs("2025-06-15T12:00:01Z [chat|none|id:latin1] peer@bench.example/phone: ", fp); static const unsigned char latin1_byte = 0xC9; fwrite(&latin1_byte, 1, 1, fp); fputs("ole\n", fp); write_normal_line(fp, 2, "after"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Expectation: the verify pass flags the line as invalid UTF-8 (ERROR), // OR ff_parse_line attempts Latin-1 fallback (in which case the line // parses and there's no error). Either is acceptable. gboolean ok = TRUE; // never crash auto_gchar gchar* note = g_strdup_printf( "errors=%d (Latin-1 byte: error or fallback OK)", s.errors); report(ctx, "F11", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F12 — empty body (e.g. receipt-only carbon) static void test_F12(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F12")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F12"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); write_normal_line(fp, 0, "before empty"); // Empty body: ends at "...phone: \n" fprintf(fp, "2025-06-15T12:00:01Z [chat|none|id:empty] peer@bench.example/phone: \n"); write_normal_line(fp, 2, "after empty"); fclose(fp); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; // Empty body is OK from parser's POV. gboolean ok = (s.errors == 0); auto_gchar gchar* note = g_strdup_printf("errors=%d (empty body OK)", s.errors); report(ctx, "F12", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F14 — mtime/inode flip: file replaced under us. ensure_fresh must rebuild. static void test_F14(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F14")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F14"); FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); for (int i = 0; i < 100; i++) write_normal_line(fp, i, "v1"); fclose(fp); ff_contact_state_t* state = ff_state_new(path); ff_state_ensure_fresh(state); size_t lines_v1 = state->total_lines; // Sleep 1 second so the mtime change is visible at second-resolution stat. sleep(1); // Now replace the file entirely (different content + different inode). auto_gchar gchar* tmp_path = g_strdup_printf("%s.swap", path); FILE* fp2 = fopen(tmp_path, "w"); fprintf(fp2, "%s", FLATFILE_HEADER); for (int i = 0; i < 250; i++) write_normal_line(fp2, i, "v2 totally different"); fclose(fp2); rename(tmp_path, path); double t0 = bench_now_ms(); int ok_fresh = ff_state_ensure_fresh(state); double dt = bench_now_ms() - t0; size_t lines_v2 = state->total_lines; ff_state_free(state); gboolean ok = ok_fresh && lines_v2 != lines_v1 && lines_v2 == 250; auto_gchar gchar* note = g_strdup_printf( "v1_lines=%zu v2_lines=%zu (expect rebuild detected and 250 lines)", lines_v1, lines_v2); report(ctx, "F14", ok, dt, path, note); } // --------------------------------------------------------------------------- // F15 — empty file static void test_F15(fail_ctx_t* ctx) { 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); double t0 = bench_now_ms(); verify_summary_t s; run_verify(&s); double dt = bench_now_ms() - t0; gboolean ok = s.infos >= 1; // expect at least one INFO auto_gchar gchar* note = g_strdup_printf( "errors=%d infos=%d warnings=%d (empty file should yield INFO)", s.errors, s.infos, s.warnings); report(ctx, "F15", ok, dt, path, note); verify_summary_free(&s); } // --------------------------------------------------------------------------- // F16 — Page-Up cursor pagination must not get stuck at file start. // // Walks the contact log from newest to oldest by repeatedly calling // db_backend_flatfile()->get_previous_chat with end_time set to the // timestamp of the oldest message returned in the previous batch // (mirroring chatwin's Page-Up flow). Stops on DB_RESPONSE_EMPTY. // // Buggy behaviour: the cursor migrates back until it lands on the byte // offset of the first index entry, then the !from_start backup logic // produces an empty [X, X) range, EMPTY arrives prematurely, and a real // UI would set WIN_SCROLL_REACHED_TOP forever. // // Pass criterion: total messages received across all calls equals the // number of messages written to the file. static void test_F16(fail_ctx_t* ctx) { 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 // (FF_INDEX_STEP=500 → 2000 messages = 4 entries). const int total_msgs = 2000; FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); for (int i = 0; i < total_msgs; i++) write_normal_line(fp, i, "page-up regression payload"); fclose(fp); g_free(g_flatfile_account_jid); g_flatfile_account_jid = g_strdup(ACCOUNT_JID); setenv("BENCH_ACCOUNT_JID", ACCOUNT_JID, 1); db_backend_t* be = db_backend_flatfile(); if (!be || !be->init || !be->get_previous_chat || !be->close) { report(ctx, "F16", FALSE, 0, path, "flatfile backend not available"); return; } // Initialize the per-contact state hash; without this _ff_get_state // returns NULL and the very first call gets DB_RESPONSE_ERROR. ProfAccount fake = { 0 }; fake.jid = (gchar*)ACCOUNT_JID; if (!be->init(&fake)) { report(ctx, "F16", FALSE, 0, path, "flatfile init failed"); return; } int total_received = 0; int empty_responses = 0; int iterations = 0; auto_gchar gchar* end_time = NULL; double t0 = bench_now_ms(); // Loop with a hard ceiling — if the bug is present we'd otherwise // stop after one page anyway, but the ceiling protects us against // a hypothetical infinite loop. while (iterations < 200) { iterations++; GSList* batch = NULL; db_history_result_t r = be->get_previous_chat( CONTACT_JID, NULL, end_time, FALSE, FALSE, &batch); if (r == DB_RESPONSE_EMPTY) { empty_responses++; break; } if (r != DB_RESPONSE_SUCCESS || !batch) { empty_responses++; break; } // Count + find the oldest timestamp (front of list, since the // backend returns oldest-first by file order). ProfMessage* oldest = batch->data; if (!oldest || !oldest->timestamp) { g_slist_free_full(batch, (GDestroyNotify)message_free); break; } int batch_n = (int)g_slist_length(batch); total_received += batch_n; g_free(end_time); end_time = g_date_time_format_iso8601(oldest->timestamp); g_slist_free_full(batch, (GDestroyNotify)message_free); } double dt = bench_now_ms() - t0; // Two regression classes are caught here: // 1. The original "cursor stuck on entries[0]" symptom (early EMPTY // after 2-3 iterations, ~200 received) — capped page-up at the top. // 2. The deeper "cursor jumping" bug — cursor was set to the oldest // line of the read window rather than the oldest line returned to // the caller after pagination, so each subsequent page-up skipped // past entire ranges of messages. With both fixed, every written // message must be reachable through sequential page-up. gboolean ok = (total_received == total_msgs) && (empty_responses == 1) && (iterations < 200); auto_gchar gchar* note = g_strdup_printf( "wrote=%d received=%d iters=%d empty=%d " "(without fix: ~200 received in ~3 iters; cursor-jumping mode: ~600 in ~7)", total_msgs, total_received, iterations, empty_responses); report(ctx, "F16", ok, dt, path, note); be->close(); } // --------------------------------------------------------------------------- // F17 — Forward iteration (start_time + from_start=TRUE) reaches every // message without gaps. // // Symmetric counterpart to F16: instead of paging from newest backwards // via end_time, page from oldest forwards via start_time. Each iteration // passes start_time = timestamp of the newest message returned in the // previous batch; the backend should return the next batch of strictly // newer messages until the file is exhausted. // // This exercises a different branch of _flatfile_get_previous_chat: // - read_from = ff_state_offset_for_time(start_time) (not bom_len) // - read_to = EOF (no end_time bisect) // - filter drops msgs with timestamp <= start_dt // - pagination keeps FIRST MESSAGES_TO_RETRIEVE (not last) // The path bypasses cursor entirely (start_time triggers cursor=-1 reset). // // Pass criterion: total messages received == total written. static void test_F17(fail_ctx_t* ctx) { if (!test_enabled(ctx->tests, "F17")) return; auto_gchar gchar* path = setup_test_dir(ctx->tmp_dir, "F17"); const int total_msgs = 2000; FILE* fp = fopen(path, "w"); fprintf(fp, "%s", FLATFILE_HEADER); for (int i = 0; i < total_msgs; i++) write_normal_line(fp, i, "forward-iter regression payload"); fclose(fp); g_free(g_flatfile_account_jid); g_flatfile_account_jid = g_strdup(ACCOUNT_JID); setenv("BENCH_ACCOUNT_JID", ACCOUNT_JID, 1); db_backend_t* be = db_backend_flatfile(); if (!be || !be->init || !be->get_previous_chat || !be->close) { report(ctx, "F17", FALSE, 0, path, "flatfile backend not available"); return; } ProfAccount fake = { 0 }; fake.jid = (gchar*)ACCOUNT_JID; if (!be->init(&fake)) { report(ctx, "F17", FALSE, 0, path, "flatfile init failed"); return; } int total_received = 0; int empty_responses = 0; int iterations = 0; // Initial start_time before the first written message so the first // call returns msgs starting at index 0. auto_gchar gchar* start_time = g_strdup("2025-06-15T11:00:00Z"); double t0 = bench_now_ms(); while (iterations < 200) { iterations++; GSList* batch = NULL; db_history_result_t r = be->get_previous_chat( CONTACT_JID, start_time, NULL, TRUE, FALSE, &batch); if (r == DB_RESPONSE_EMPTY) { empty_responses++; break; } if (r != DB_RESPONSE_SUCCESS || !batch) { empty_responses++; break; } // pre_result is sorted oldest-first; with from_start=TRUE we keep // the first MESSAGES_TO_RETRIEVE. The newest in the returned batch // is therefore the LAST element of the list. ProfMessage* newest = NULL; int batch_n = 0; for (GSList* l = batch; l; l = l->next) { batch_n++; newest = l->data; } if (!newest || !newest->timestamp) { g_slist_free_full(batch, (GDestroyNotify)message_free); break; } total_received += batch_n; g_free(start_time); start_time = g_date_time_format_iso8601(newest->timestamp); g_slist_free_full(batch, (GDestroyNotify)message_free); } double dt = bench_now_ms() - t0; gboolean ok = (total_received == total_msgs) && (empty_responses == 1) && (iterations < 200); auto_gchar gchar* note = g_strdup_printf( "wrote=%d received=%d iters=%d empty=%d (forward-iter regression)", total_msgs, total_received, iterations, empty_responses); report(ctx, "F17", ok, dt, path, note); be->close(); } // --------------------------------------------------------------------------- // Driver int main(int argc, char** argv) { fail_ctx_t ctx = { 0 }; ctx.tmp_dir = "/tmp/cproof-bench-failmodes"; ctx.csv_path = NULL; ctx.tests = "all"; 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; else { fprintf(stderr, "unknown option: %s\n", a); return 2; } } if (g_mkdir_with_parents(ctx.tmp_dir, 0755) != 0) { fprintf(stderr, "cannot create %s\n", ctx.tmp_dir); return 2; } fprintf(stderr, "===== bench_failure_modes: tmp=%s =====\n", ctx.tmp_dir); test_F1(&ctx); test_F2(&ctx); test_F3(&ctx); test_F7(&ctx); test_F8(&ctx); test_F9(&ctx); test_F10(&ctx); test_F11(&ctx); test_F12(&ctx); test_F14(&ctx); test_F15(&ctx); test_F16(&ctx); test_F17(&ctx); fprintf(stderr, "\nfailure-modes summary: %d passed, %d failed\n", ctx.passed, ctx.failed); g_free(g_flatfile_account_jid); g_flatfile_account_jid = NULL; return ctx.failed == 0 ? 0 : 1; }