test(db2-7): pin compaction + migration temps beside a file-form log

- test_file_form_temps_beside_log (runtime/test/test_wal.c): the log is
  `<dir>/app.db` — an operator's name, not shard-0.wal — with a sibling
  directory `app.db.d/` as the decoy nothing may land in.
- Proof by blocker: a DIRECTORY planted at exactly `<file>.compact` makes
  `wo_wal_compact` and `wo_wal_migrate` each return -1 with the log
  untouched (record count unchanged, blocker still an empty dir); a temp
  anywhere else would have let them succeed.
- Blocker removed: compaction 10 → 2 records, migration n,t → n,t,extra
  succeeds, `<file>.compact` gone after each rename, the directory holds
  exactly {app.db, app.db.d}, the decoy is empty, the migrated file
  replays into the new shape (slots[0] == 107, slots[2] == 0).
- The parent fsync'd after a rename is `parent_dir_of(<file>)`, the helper
  the resolver shares (task 1), so its derivation is pinned there; fsync
  itself is not observable from a test.
- Green on first run (57 assertions) as a pin must be; teeth shown by a
  mutation control — compaction's temp redirected into the decoy turned
  14 assertions red (`wo_wal_compact(&w, &db) == 0, want -1`, …).
- `make -C runtime test`: 21 suites, 8431 pass / 0 fail (was 8374/0).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ccee2d04fddfb96c7dfab1c17f235e3a9bbc69fb)
This commit is contained in:
shoney.arickathil 2026-09-10 14:18:36 +02:00
parent c19a01564f
commit 587991124d

View file

@ -6,6 +6,7 @@
* present with the right contents. */
#define _POSIX_C_SOURCE 200809L
#include <dirent.h>
#include <fcntl.h>
#include <signal.h>
#include <stdlib.h>
@ -1430,6 +1431,104 @@ static void test_migrate_crash_before_rename(void) {
wo_db_destroy(&db2);
}
/* databasev2 7 Task 2: with WO_DATA naming a FILE, compaction (databasev2 3)
* and migration (databasev2 12) must build their temp as `<file>.compact`
* beside it and take the parent they fsync from the FILE's path — both derive
* everything from the log path today, and this pins that against a future
* "derive it from WO_DATA". The proof is a blocker, not a listing: a
* DIRECTORY planted at exactly `<file>.compact` makes each rewrite refuse (-1)
* with the log untouched, which a temp anywhere else could not produce; with
* the blocker gone both succeed and the operator's file is the only artifact
* in its directory (a sibling directory stands in as the decoy nothing may
* land in). The parent derivation is one helper shared with the resolver
* (parent_dir_of), so test_resolve_data_path's missing-parent arm already
* pins what "the parent" of such a path is; fsync itself is not observable. */
static int dir_entries(const char *dir) {
DIR *d = opendir(dir);
if (!d) return -1;
int n = 0;
struct dirent *e;
while ((e = readdir(d)) != NULL)
if (strcmp(e->d_name, ".") != 0 && strcmp(e->d_name, "..") != 0) n++;
closedir(d);
return n;
}
static void test_file_form_temps_beside_log(void) {
char dir[160], decoy[192], path[192], tmp[224], out[256];
snprintf(dir, sizeof dir, "%s/fileform", g_dir);
snprintf(decoy, sizeof decoy, "%s/app.db.d", dir); /* sibling directory */
snprintf(path, sizeof path, "%s/app.db", dir); /* the operator's name */
snprintf(tmp, sizeof tmp, "%s%s", path, WO_WAL_TMP_SUFFIX);
T_EQ(mkdir(dir, 0700), 0);
T_EQ(mkdir(decoy, 0700), 0);
T_EQ(wo_wal_resolve_data_path(path, out, sizeof out), 0);
T_STREQ(out, path); /* the file form hands the path straight to the engine */
/* ---- compaction ---- */
wo_rt rt;
T_EQ(wo_rt_init(&rt, 1 << 20, CLASSES, 1), 0);
wo_db db;
T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0);
wo_wal w;
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
const char *msg = "";
uint64_t ids[2];
for (int i = 0; i < 2; i++) {
wo_str *s = wo_str_new(&rt, "abc", 3);
uint64_t vals[2] = {(uint64_t)(i + 1), (uint64_t)(uintptr_t)s};
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
T_EQ(wo_wal_commit(&w), 0);
}
for (int k = 0; k < 8; k++) {
int ek = 0;
T_EQ(wo_row_update_field(&db, 0, ids[0], 0, (uint64_t)(100 + k), &msg, &ek), 0);
T_EQ(wo_wal_append_update(&w, &db, 0, ids[0]), 0);
T_EQ(wo_wal_commit(&w), 0);
}
T_CHECK(wo_wal_check(path, NULL) == 10);
T_EQ(mkdir(tmp, 0700), 0); /* the blocker */
T_EQ(wo_wal_compact(&w, &db), -1); /* the temp has exactly one home */
T_CHECK(wo_wal_check(path, NULL) == 10); /* refused = untouched */
T_EQ(rmdir(tmp), 0); /* still an empty dir: nothing went in */
T_EQ(wo_wal_compact(&w, &db), 0);
T_CHECK(wo_wal_check(path, NULL) == 2);
T_CHECK(access(tmp, F_OK) != 0);
T_EQ(dir_entries(dir), 2); /* app.db + the decoy, nothing else */
T_EQ(dir_entries(decoy), 0); /* and the decoy saw nothing */
wo_wal_close(&w);
wo_db_destroy(&db);
wo_rt_destroy(&rt);
/* ---- migration: same temp, same parent. The compacted log is two legacy
INSERT records of (scalar, text) — MIG_NT's shape — so it migrates
n,t → n,t,extra in place. ---- */
wo_schema_class oc[] = {SC("row", 0, mig_sf_nt)};
wo_schema oldsc = {1, oc, NULL};
wo_schema_class nc[] = {SC("row", 0, mig_sf_nte)};
wo_schema newsc = {1, nc, NULL};
wo_mig_plan pl;
T_EQ(wo_schema_diff(&oldsc, &newsc, &pl), 0);
wo_db dbn;
T_EQ(wo_db_init(&dbn, MIG_NTE, 1, 0, 1), 0);
T_EQ(mkdir(tmp, 0700), 0);
T_EQ(wo_wal_migrate(path, &dbn, &oldsc, &pl, &newsc, 0, NULL), -1);
T_CHECK(wo_wal_check(path, NULL) == 2); /* refused = untouched */
T_EQ(rmdir(tmp), 0);
T_EQ(wo_wal_migrate(path, &dbn, &oldsc, &pl, &newsc, 0, NULL), 0);
wo_mig_plan_free(&pl);
T_CHECK(access(tmp, F_OK) != 0);
T_EQ(dir_entries(dir), 2);
T_EQ(dir_entries(decoy), 0);
/* and the file, under the operator's name, replays into the new shape */
T_EQ(wo_wal_replay(path, &dbn), 2);
db_row *r = wo_row_ptr(&dbn, 0, ids[0]);
T_CHECK(r != NULL && r->slots[0] == 107 && r->slots[2] == 0);
wo_db_destroy(&dbn);
}
/* POISON BITES ONLY WITH RECORDS: a retyped class with no stored rows never
* blocks the boot; the same retype WITH a row refuses and names the field */
static void test_migrate_poison_needs_records(void) {
@ -3512,6 +3611,7 @@ int main(void) {
test_keys_resident_delete_then_replay();
test_stale_compact_temp_is_removed();
test_resolve_data_path();
test_file_form_temps_beside_log();
test_should_compact_policy();
test_compact_refuses_with_staged_records();
test_torn_tail();