Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

179 lines
5.8 KiB
C

/*
* test_store_bulk.c — Crash-safety tests for bulk write mode.
*
* Verifies that cbm_store_begin_bulk / cbm_store_end_bulk never switch away
* from WAL journal mode. Switching to MEMORY journal mode during bulk writes
* makes the database unrecoverable on a crash because the in-memory rollback
* journal is lost. WAL mode is inherently crash-safe: uncommitted WAL entries
* are discarded on the next open.
*
* Tests:
* bulk_pragma_wal_invariant — journal_mode stays "wal" after begin_bulk
* bulk_pragma_end_wal_invariant — journal_mode stays "wal" after end_bulk
* bulk_crash_recovery — DB is readable after simulated crash mid-bulk
*/
#include "test_framework.h"
#include <store/store.h>
#include <foundation/compat.h>
#include <sqlite3.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef _WIN32
#include <unistd.h>
#include <sys/wait.h>
#endif
/* ── Helpers ──────────────────────────────────────────────────── */
/* Query journal_mode via a separate read-only connection so the result is
* independent of any state held inside the cbm_store_t under test. */
static char *get_journal_mode(const char *db_path) {
sqlite3 *db;
if (sqlite3_open_v2(db_path, &db, SQLITE_OPEN_READONLY, NULL) != SQLITE_OK)
return NULL;
sqlite3_stmt *stmt;
char *mode = NULL;
if (sqlite3_prepare_v2(db, "PRAGMA journal_mode;", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW)
mode = strdup((const char *)sqlite3_column_text(stmt, 0));
sqlite3_finalize(stmt);
}
sqlite3_close(db);
return mode;
}
static void make_temp_path(char *buf, size_t n) {
snprintf(buf, n, "%s/cmm_bulk_test_%d.db", cbm_tmpdir(), (int)getpid());
}
static void cleanup_db(const char *path) {
remove(path);
char aux[512];
snprintf(aux, sizeof(aux), "%s-wal", path);
remove(aux);
snprintf(aux, sizeof(aux), "%s-shm", path);
remove(aux);
}
/* ── Tests ──────────────────────────────────────────────────────── */
/* begin_bulk must NOT switch journal_mode away from WAL. */
TEST(bulk_pragma_wal_invariant) {
char db_path[256];
make_temp_path(db_path, sizeof(db_path));
cleanup_db(db_path);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
char *before = get_journal_mode(db_path);
ASSERT_NOT_NULL(before);
ASSERT_STR_EQ(before, "wal");
free(before);
int rc = cbm_store_begin_bulk(s);
ASSERT_EQ(rc, CBM_STORE_OK);
char *after = get_journal_mode(db_path);
ASSERT_NOT_NULL(after);
ASSERT_STR_EQ(after, "wal"); /* FAILS with bug, PASSES with fix */
free(after);
cbm_store_end_bulk(s);
cbm_store_close(s);
cleanup_db(db_path);
PASS();
}
/* end_bulk must also leave journal_mode as WAL. */
TEST(bulk_pragma_end_wal_invariant) {
char db_path[256];
make_temp_path(db_path, sizeof(db_path));
cleanup_db(db_path);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
cbm_store_begin_bulk(s);
cbm_store_end_bulk(s);
char *mode = get_journal_mode(db_path);
ASSERT_NOT_NULL(mode);
ASSERT_STR_EQ(mode, "wal");
free(mode);
cbm_store_close(s);
cleanup_db(db_path);
PASS();
}
/* Simulate a crash mid-bulk-write: fork a child that calls begin_bulk, opens
* an explicit transaction, and then calls _exit() without committing or calling
* end_bulk. The parent verifies the database is still openable and that
* committed baseline data is intact and uncommitted data is absent.
*
* This test uses fork()/waitpid() and is therefore POSIX-only. */
#ifndef _WIN32
TEST(bulk_crash_recovery) {
char db_path[256];
make_temp_path(db_path, sizeof(db_path));
cleanup_db(db_path);
/* Write committed baseline data. */
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = cbm_store_upsert_project(s, "baseline", "/tmp/baseline");
ASSERT_EQ(rc, CBM_STORE_OK);
cbm_store_close(s);
/* Child: enter bulk mode, start a transaction, write, then crash. */
pid_t pid = fork();
if (pid == 0) {
cbm_store_t *cs = cbm_store_open_path(db_path);
if (!cs)
_exit(1);
cbm_store_begin_bulk(cs);
cbm_store_begin(cs); /* explicit open transaction */
cbm_store_upsert_project(cs, "crashed", "/tmp/crashed");
/* Crash: no COMMIT, no end_bulk, no close. */
_exit(0);
}
ASSERT_GT(pid, 0);
int status;
waitpid(pid, &status, 0);
/* Confirm child exited normally so the write actually occurred. */
ASSERT(WIFEXITED(status) && WEXITSTATUS(status) == 0);
/* Recovery: database must open cleanly. */
cbm_store_t *recovered = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(recovered); /* NULL would indicate corruption */
/* Baseline commit must survive. */
cbm_project_t p = {0};
rc = cbm_store_get_project(recovered, "baseline", &p);
ASSERT_EQ(rc, CBM_STORE_OK);
ASSERT_STR_EQ(p.name, "baseline");
cbm_project_free_fields(&p);
/* Uncommitted "crashed" write must NOT appear after recovery. */
cbm_project_t p2 = {0};
int rc2 = cbm_store_get_project(recovered, "crashed", &p2);
ASSERT_NEQ(rc2, CBM_STORE_OK); /* row must be absent */
cbm_store_close(recovered);
cleanup_db(db_path);
PASS();
}
#endif /* _WIN32 */
/* ── Suite ──────────────────────────────────────────────────────── */
SUITE(store_bulk) {
RUN_TEST(bulk_pragma_wal_invariant);
RUN_TEST(bulk_pragma_end_wal_invariant);
#ifndef _WIN32
RUN_TEST(bulk_crash_recovery);
#endif
}