Task 5a of docs/superpowers/plans/2026-08-26-table-residency.md. The read path itself is NOT in this commit; see the note below. - the offset problem is far smaller than the spec feared. `w->off` is the durable tail and `w->len` the staged bytes, and wo_wal_commit pwrites the whole batch AT off before advancing it — so a record staged now lands at exactly off+len, knowable at append time with no deferral to flush - shipped as an inline accessor rather than new out-params on the three append functions, so the 156 existing WAL checks keep their signatures - correct across both awkward cases, and both are now unit-pinned: a failed commit leaves off unadvanced so the record still lands where it was promised, and wo_wal_open positions off at the end of the INTACT prefix so offsets are always relative to validated data - test_offset_capture asserts the recovered ID per record, not merely that a record parses — a wrong offset reads a NEIGHBOURING record, which passes its own CRC and returns the wrong row silently. 400 records across repeated buffer growth (stage() doubles from 4096) and uneven commit batches, so offsets are exercised mid-buffer and right after a flush The failed-commit test caught MY OWN misunderstanding: I asserted next_offset was unchanged after a failed commit. It is not, and should not be — the record is still staged, so next_offset correctly points PAST it. The invariant that matters is that the durable tail did not move, which is what it now asserts. Gates: test_wal 3428/0 (was 3426), all 18 runtime suites 0 fail under ASan+UBSan, cli_smoke OK, oop-e2e 119/0, residency 8/0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
460 lines
17 KiB
C
460 lines
17 KiB
C
/* test_wal — iteration 9 Task 2: typed WAL + boot replay.
|
|
* Round-trip through a replay, torn-tail drop, reopen-overwrites-tear,
|
|
* and the commit-then-kill crash battery: a forked child inserts rows and
|
|
* acks each COMMITTED id over a pipe; SIGKILL lands mid-stream; the parent
|
|
* verifies with the offline oracle and a replay that every acked id is
|
|
* present with the right contents. */
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include <fcntl.h>
|
|
#include <signal.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include "gc.h"
|
|
#include "obj.h"
|
|
#include "t.h"
|
|
#include "table.h"
|
|
#include "wal.h"
|
|
|
|
/* class 0: Row { n: scalar, label: Text } */
|
|
static const uint8_t row_kinds[] = {WO_K_SCALAR, WO_K_TEXT};
|
|
static const wo_classdesc CLASSES[] = {
|
|
{.name = 0, .flags = 0, .field_cnt = 2, .kinds = row_kinds},
|
|
};
|
|
|
|
static char g_dir[64];
|
|
|
|
static void test_roundtrip_replay(void) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/basic.wal", g_dir);
|
|
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 = "";
|
|
|
|
/* three inserts and one remove, RAM first, WAL second, one commit */
|
|
uint64_t ids[3];
|
|
for (int i = 0; i < 3; i++) {
|
|
wo_str *s = wo_str_new(&rt, "abcXYZ" + i, 3); /* "abc","bcX","cXY" */
|
|
uint64_t vals[2] = {(uint64_t)(i * 10), (uint64_t)(uintptr_t)s};
|
|
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
T_CHECK(ids[i] != 0);
|
|
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
|
wo_str_free(&rt, s);
|
|
}
|
|
T_EQ(wo_row_remove(&db, 0, ids[1]), 0);
|
|
T_EQ(wo_wal_append_remove(&w, 0, ids[1]), 0);
|
|
T_EQ(wo_wal_commit(&w), 0);
|
|
wo_wal_close(&w);
|
|
wo_db_destroy(&db);
|
|
|
|
/* boot: fresh engine, replay, deep-compare */
|
|
wo_db db2;
|
|
T_EQ(wo_db_init(&db2, CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay(path, &db2), 4);
|
|
uint64_t out[2];
|
|
T_EQ(wo_row_read(&db2, &rt, 0, ids[0], out, &msg), 0);
|
|
T_EQ(out[0], 0);
|
|
wo_str *s0 = (wo_str *)(uintptr_t)out[1];
|
|
T_CHECK(s0->len == 3 && memcmp(s0->data, "abc", 3) == 0);
|
|
wo_str_free(&rt, s0);
|
|
T_EQ(wo_row_read(&db2, &rt, 0, ids[1], out, &msg), -1); /* removed */
|
|
T_EQ(wo_row_read(&db2, &rt, 0, ids[2], out, &msg), 0);
|
|
T_EQ(out[0], 20);
|
|
wo_str_free(&rt, (wo_str *)(uintptr_t)out[1]);
|
|
/* next_id advanced past the replayed ids: a fresh insert never collides */
|
|
uint64_t vals[2] = {99, 0};
|
|
uint64_t fresh = wo_row_insert(&db2, 0, vals, &msg, NULL);
|
|
T_CHECK(fresh > ids[2]);
|
|
wo_db_destroy(&db2);
|
|
|
|
/* update record: re-log, replay replaces */
|
|
{
|
|
char upath[128];
|
|
snprintf(upath, sizeof upath, "%s/upd.wal", g_dir);
|
|
wo_db du;
|
|
T_EQ(wo_db_init(&du, CLASSES, 1, 0, 1), 0);
|
|
wo_wal wu;
|
|
T_EQ(wo_wal_open(&wu, upath, 0), 0);
|
|
wo_str *s1 = wo_str_new(&rt, "old", 3);
|
|
uint64_t uv[2] = {7, (uint64_t)(uintptr_t)s1};
|
|
uint64_t uid = wo_row_insert(&du, 0, uv, &msg, NULL);
|
|
T_EQ(wo_wal_append_insert(&wu, &du, 0, uid), 0);
|
|
int ek = 0;
|
|
wo_str *s2 = wo_str_new(&rt, "new!", 4);
|
|
T_EQ(wo_row_update_field(&du, 0, uid, 1, (uint64_t)(uintptr_t)s2, &msg, &ek), 0);
|
|
T_EQ(wo_row_update_field(&du, 0, uid, 0, 8, &msg, &ek), 0);
|
|
T_EQ(wo_wal_append_update(&wu, &du, 0, uid), 0);
|
|
T_EQ(wo_wal_commit(&wu), 0);
|
|
wo_wal_close(&wu);
|
|
wo_db_destroy(&du);
|
|
wo_db db4;
|
|
T_EQ(wo_db_init(&db4, CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay(upath, &db4), 2);
|
|
uint64_t uo[2];
|
|
T_EQ(wo_row_read(&db4, &rt, 0, uid, uo, &msg), 0);
|
|
T_EQ(uo[0], 8);
|
|
wo_str *us = (wo_str *)(uintptr_t)uo[1];
|
|
T_CHECK(us->len == 4 && memcmp(us->data, "new!", 4) == 0);
|
|
wo_str_free(&rt, us);
|
|
wo_drop_obj(&rt, (wo_hdr *)s1);
|
|
wo_drop_obj(&rt, (wo_hdr *)s2);
|
|
wo_db_destroy(&db4);
|
|
}
|
|
|
|
/* replay of a missing file is a fresh boot, not an error */
|
|
wo_db db3;
|
|
T_EQ(wo_db_init(&db3, CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay("/nonexistent/nope.wal", &db3), 0);
|
|
wo_db_destroy(&db3);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
static void test_torn_tail(void) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/torn.wal", g_dir);
|
|
wo_db db;
|
|
T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0);
|
|
wo_wal w;
|
|
T_EQ(wo_wal_open(&w, path, 0), 0);
|
|
const char *msg = "";
|
|
for (int i = 0; i < 5; i++) {
|
|
uint64_t vals[2] = {(uint64_t)i, 0};
|
|
uint64_t id = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
T_EQ(wo_wal_append_insert(&w, &db, 0, id), 0);
|
|
T_EQ(wo_wal_commit(&w), 0);
|
|
}
|
|
uint64_t intact_end = w.off;
|
|
/* tear: append half a record's worth of a valid-looking header + junk */
|
|
uint32_t fake_len = 40, fake_crc = 0xDEAD;
|
|
uint8_t junk[20] = {7, 7, 7};
|
|
T_CHECK(pwrite(w.fd, &fake_len, 4, (off_t)intact_end) == 4);
|
|
T_CHECK(pwrite(w.fd, &fake_crc, 4, (off_t)(intact_end + 4)) == 4);
|
|
T_CHECK(pwrite(w.fd, junk, sizeof junk, (off_t)(intact_end + 8)) == (ssize_t)sizeof junk);
|
|
wo_wal_close(&w);
|
|
wo_db_destroy(&db);
|
|
|
|
/* the oracle sees exactly the intact prefix */
|
|
uint64_t at = 0;
|
|
T_EQ(wo_wal_check(path, &at), 5);
|
|
T_EQ(at, intact_end);
|
|
|
|
/* replay drops the tear whole */
|
|
wo_db db2;
|
|
T_EQ(wo_db_init(&db2, CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay(path, &db2), 5);
|
|
T_EQ(db2.tables[0].count, 5);
|
|
wo_db_destroy(&db2);
|
|
|
|
/* reopen positions AT the tear: the next commit overwrites it */
|
|
wo_db db3;
|
|
T_EQ(wo_db_init(&db3, CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay(path, &db3), 5);
|
|
wo_wal w2;
|
|
T_EQ(wo_wal_open(&w2, path, 0), 0);
|
|
T_EQ(w2.off, intact_end);
|
|
uint64_t vals[2] = {100, 0};
|
|
uint64_t id = wo_row_insert(&db3, 0, vals, &msg, NULL);
|
|
T_EQ(wo_wal_append_insert(&w2, &db3, 0, id), 0);
|
|
T_EQ(wo_wal_commit(&w2), 0);
|
|
wo_wal_close(&w2);
|
|
T_EQ(wo_wal_check(path, NULL), 6); /* tear gone, record in its place */
|
|
wo_db_destroy(&db3);
|
|
}
|
|
|
|
/* ---- the crash battery -------------------------------------------------- */
|
|
|
|
/* Child: insert forever — RAM, WAL, COMMIT, and only then ack the id down
|
|
* the pipe. Killed mid-stream by the parent. */
|
|
static void battery_child(const char *path, int ack_fd) {
|
|
wo_rt rt;
|
|
wo_db db;
|
|
wo_wal w;
|
|
if (wo_rt_init(&rt, 1 << 20, CLASSES, 1) != 0) _exit(9);
|
|
if (wo_db_init(&db, CLASSES, 1, 0, 1) != 0) _exit(9);
|
|
if (wo_wal_open(&w, path, 1 << 20) != 0) _exit(9);
|
|
const char *msg = "";
|
|
for (uint64_t i = 0;; i++) {
|
|
char label[32];
|
|
int n = snprintf(label, sizeof label, "row-%llu", (unsigned long long)i);
|
|
wo_str *s = wo_str_new(&rt, label, (uint32_t)n);
|
|
uint64_t vals[2] = {i * 3 + 1, (uint64_t)(uintptr_t)s};
|
|
uint64_t id = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
wo_str_free(&rt, s);
|
|
if (!id) _exit(9);
|
|
if (wo_wal_append_insert(&w, &db, 0, id) != 0) _exit(9);
|
|
if (wo_wal_commit(&w) != 0) _exit(9); /* durable BEFORE the ack */
|
|
ssize_t wr = write(ack_fd, &id, 8);
|
|
if (wr != 8) _exit(0); /* parent went away */
|
|
}
|
|
}
|
|
|
|
static void test_crash_battery(void) {
|
|
for (int round = 0; round < 5; round++) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/crash-%d.wal", g_dir, round);
|
|
int pipefd[2];
|
|
T_EQ(pipe(pipefd), 0);
|
|
pid_t pid = fork();
|
|
T_CHECK(pid >= 0);
|
|
if (pid == 0) {
|
|
close(pipefd[0]);
|
|
battery_child(path, pipefd[1]);
|
|
_exit(0);
|
|
}
|
|
close(pipefd[1]);
|
|
/* collect acks for a few ms, then kill mid-stream — no sync with
|
|
the child's commit loop, which is the point */
|
|
struct timespec ts = {0, (20 + round * 13) * 1000000L};
|
|
while (nanosleep(&ts, &ts) != 0) {}
|
|
kill(pid, SIGKILL);
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
/* drain every ack that made it into the pipe */
|
|
uint64_t acked[65536];
|
|
size_t n_acked = 0;
|
|
for (;;) {
|
|
uint64_t id;
|
|
ssize_t n = read(pipefd[0], &id, 8);
|
|
if (n != 8) break;
|
|
if (n_acked < 65536) acked[n_acked++] = id;
|
|
}
|
|
close(pipefd[0]);
|
|
T_CHECK(n_acked > 0); /* the child got at least one commit out */
|
|
|
|
/* offline oracle: the file's intact prefix covers every ack */
|
|
int64_t intact = wo_wal_check(path, NULL);
|
|
T_CHECK(intact >= (int64_t)n_acked);
|
|
|
|
/* replay and verify: every acked id present, contents exact */
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 22, CLASSES, 1), 0);
|
|
wo_db db;
|
|
T_EQ(wo_db_init(&db, CLASSES, 1, 0, 1), 0);
|
|
int64_t applied = wo_wal_replay(path, &db);
|
|
T_CHECK(applied >= (int64_t)n_acked);
|
|
const char *msg = "";
|
|
int bad = 0;
|
|
for (size_t i = 0; i < n_acked; i++) {
|
|
uint64_t out[2];
|
|
if (wo_row_read(&db, &rt, 0, acked[i], out, &msg) != 0) {
|
|
bad++;
|
|
continue;
|
|
}
|
|
/* id = i+1 (shard 0 of 1), field 0 = i*3+1, label = "row-i" */
|
|
char want[32];
|
|
int wl = snprintf(want, sizeof want, "row-%llu",
|
|
(unsigned long long)(acked[i] - 1));
|
|
wo_str *s = (wo_str *)(uintptr_t)out[1];
|
|
if (out[0] != (acked[i] - 1) * 3 + 1 || s->len != (uint32_t)wl ||
|
|
memcmp(s->data, want, (size_t)wl) != 0)
|
|
bad++;
|
|
wo_str_free(&rt, s);
|
|
}
|
|
T_EQ(bad, 0); /* zero acked-but-missing, zero acked-but-wrong */
|
|
wo_db_destroy(&db);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
}
|
|
|
|
/* iteration 19: a Float column and a Bytes column survive a WAL round trip
|
|
* BIT-EXACT. Bit-exact is the whole assertion — the durability path must not
|
|
* render a float as decimal anywhere, or NaN, the infinities and -0.0 would
|
|
* each come back as something else. Bytes goes through the same length-
|
|
* prefixed blob a Text does and must come back as a Bytes, not a Text. */
|
|
static const uint8_t fb_kinds[] = {WO_K_FLOAT, WO_K_BYTES};
|
|
static const wo_classdesc FB_CLASSES[] = {
|
|
{.name = 0, .flags = 0, .field_cnt = 2, .kinds = fb_kinds},
|
|
};
|
|
|
|
static void test_float_bytes_replay(void) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/floatbytes.wal", g_dir);
|
|
wo_rt rt;
|
|
T_EQ(wo_rt_init(&rt, 1 << 20, FB_CLASSES, 1), 0);
|
|
wo_db db;
|
|
T_EQ(wo_db_init(&db, FB_CLASSES, 1, 0, 1), 0);
|
|
wo_wal w;
|
|
T_EQ(wo_wal_open(&w, path, 1 << 16), 0);
|
|
const char *msg = "";
|
|
|
|
/* the values a decimal round trip would destroy, plus a NUL-bearing blob
|
|
* that a NUL-terminated string path would truncate */
|
|
const double vals_f[] = {9.99, 0.0 / 0.0, 1.0 / 0.0, -1.0 / 0.0, -0.0, 1e308};
|
|
const char blob[] = {'a', '\0', 'b'};
|
|
enum { N = sizeof vals_f / sizeof vals_f[0] };
|
|
uint64_t ids[N];
|
|
for (int i = 0; i < N; i++) {
|
|
wo_str *b = wo_bytes_new(&rt, blob, sizeof blob);
|
|
T_CHECK(b != NULL);
|
|
uint64_t vals[2] = {wo_bits(vals_f[i]), (uint64_t)(uintptr_t)b};
|
|
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
T_CHECK(ids[i] != 0);
|
|
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
|
wo_str_free(&rt, b);
|
|
}
|
|
T_EQ(wo_wal_commit(&w), 0);
|
|
wo_wal_close(&w);
|
|
wo_db_destroy(&db);
|
|
|
|
wo_db db2;
|
|
T_EQ(wo_db_init(&db2, FB_CLASSES, 1, 0, 1), 0);
|
|
T_EQ(wo_wal_replay(path, &db2), N);
|
|
for (int i = 0; i < N; i++) {
|
|
uint64_t out[2];
|
|
T_EQ(wo_row_read(&db2, &rt, 0, ids[i], out, &msg), 0);
|
|
/* BITS, not value: NaN != NaN and -0.0 == 0.0, so a value comparison
|
|
* would pass while silently having lost the payload or the sign */
|
|
T_EQ(out[0], wo_bits(vals_f[i]));
|
|
wo_str *b = (wo_str *)(uintptr_t)out[1];
|
|
T_CHECK(b != NULL);
|
|
T_EQ(b->h.class_id, WO_CLS_BYTES); /* a Bytes column yields a Bytes */
|
|
T_CHECK(b->len == sizeof blob && memcmp(b->data, blob, sizeof blob) == 0);
|
|
wo_str_free(&rt, b);
|
|
}
|
|
wo_db_destroy(&db2);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
|
|
/* databasev2 2: offset capture. wo_wal_next_offset must name exactly where a
|
|
* record lands, so a resident:keys table can read it back by that offset
|
|
* later. A wrong offset is the worst possible bug here: it reads a
|
|
* NEIGHBOURING record, which passes its own CRC and returns the wrong row
|
|
* silently. So this asserts the recovered id per record, not just that a
|
|
* record parses.
|
|
*
|
|
* Covers the two awkward cases the design called out: records straddling a
|
|
* buffer growth (stage() doubles from 4096, so 400 rows with Text payloads
|
|
* cross it repeatedly), and a batch spanning several commits. */
|
|
static void test_offset_capture(void) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/offsets.wal", g_dir);
|
|
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 = "";
|
|
|
|
enum { N = 400 };
|
|
uint64_t ids[N], offs[N];
|
|
|
|
/* commit in uneven batches so offsets are exercised both mid-buffer and
|
|
* immediately after a flush reset len to 0 */
|
|
for (int i = 0; i < N; i++) {
|
|
char lbl[32];
|
|
int ln = snprintf(lbl, sizeof lbl, "label-%d-padding", i);
|
|
wo_str *s = wo_str_new(&rt, lbl, (uint32_t)ln);
|
|
uint64_t vals[2] = {(uint64_t)i, (uint64_t)(uintptr_t)s};
|
|
ids[i] = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
T_CHECK(ids[i] != 0);
|
|
/* BEFORE the append: this is the contract */
|
|
offs[i] = wo_wal_next_offset(&w);
|
|
T_EQ(wo_wal_append_insert(&w, &db, 0, ids[i]), 0);
|
|
wo_str_free(&rt, s);
|
|
if (i % 7 == 6) T_EQ(wo_wal_commit(&w), 0);
|
|
}
|
|
T_EQ(wo_wal_commit(&w), 0);
|
|
|
|
/* offsets must be strictly increasing and inside the written region */
|
|
for (int i = 1; i < N; i++) T_CHECK(offs[i] > offs[i - 1]);
|
|
|
|
/* read each record back BY ITS REPORTED OFFSET and check the id matches:
|
|
* payload is [kind u8][class u32][id u64], after the 8-byte len+crc head */
|
|
int checked = 0;
|
|
for (int i = 0; i < N; i++) {
|
|
uint8_t head[8], body[13];
|
|
T_EQ((int)pread(w.fd, head, 8, (off_t)offs[i]), 8);
|
|
T_EQ((int)pread(w.fd, body, 13, (off_t)(offs[i] + 8)), 13);
|
|
T_EQ(body[0], WO_WAL_INSERT);
|
|
uint32_t cid;
|
|
uint64_t rid;
|
|
memcpy(&cid, body + 1, 4);
|
|
memcpy(&rid, body + 5, 8);
|
|
T_EQ(cid, 0u);
|
|
T_EQ(rid, ids[i]);
|
|
checked++;
|
|
}
|
|
T_EQ(checked, N);
|
|
|
|
wo_wal_close(&w);
|
|
wo_db_destroy(&db);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
/* databasev2 2: an offset reported for a record whose commit FAILED must
|
|
* never be trusted. Simulated by closing the fd under the wal so pwrite
|
|
* fails: the offset accessor must not have advanced past the durable tail,
|
|
* so a later successful commit reuses the same place. */
|
|
static void test_offset_after_failed_commit(void) {
|
|
char path[128];
|
|
snprintf(path, sizeof path, "%s/offfail.wal", g_dir);
|
|
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 vals[2] = {7u, 0u};
|
|
uint64_t id1 = wo_row_insert(&db, 0, vals, &msg, NULL);
|
|
T_CHECK(id1 != 0);
|
|
uint64_t at1 = wo_wal_next_offset(&w);
|
|
T_EQ(wo_wal_append_insert(&w, &db, 0, id1), 0);
|
|
|
|
/* break the fd, so the commit cannot succeed */
|
|
int saved = dup(w.fd);
|
|
T_CHECK(saved >= 0);
|
|
close(w.fd);
|
|
w.fd = -1;
|
|
T_CHECK(wo_wal_commit(&w) != 0);
|
|
/* The DURABLE TAIL is what must not move. `next_offset` legitimately
|
|
* points PAST the still-staged record (off unchanged, len still holding
|
|
* it) — asserting otherwise was this test's own first mistake. The
|
|
* invariant that matters: off is untouched, so the record still lands at
|
|
* the offset already reported for it. */
|
|
T_EQ(w.off, at1);
|
|
|
|
/* restore and commit for real: the record lands exactly where promised */
|
|
w.fd = saved;
|
|
T_EQ(wo_wal_commit(&w), 0);
|
|
uint8_t body[13];
|
|
T_EQ((int)pread(w.fd, body, 13, (off_t)(at1 + 8)), 13);
|
|
uint64_t rid;
|
|
memcpy(&rid, body + 5, 8);
|
|
T_EQ(rid, id1);
|
|
|
|
wo_wal_close(&w);
|
|
wo_db_destroy(&db);
|
|
wo_rt_destroy(&rt);
|
|
}
|
|
|
|
int main(void) {
|
|
snprintf(g_dir, sizeof g_dir, "/tmp/wo-wal-test-XXXXXX");
|
|
if (!mkdtemp(g_dir)) return 1;
|
|
test_roundtrip_replay();
|
|
test_torn_tail();
|
|
test_float_bytes_replay();
|
|
test_offset_capture();
|
|
test_offset_after_failed_commit();
|
|
test_crash_battery();
|
|
/* leave the dir for a failed run's forensics only */
|
|
if (!t_fail) {
|
|
char cmd[128];
|
|
snprintf(cmd, sizeof cmd, "rm -rf %s", g_dir);
|
|
if (system(cmd) != 0) fprintf(stderr, "cleanup failed, kept %s\n", g_dir);
|
|
} else {
|
|
fprintf(stderr, "kept %s\n", g_dir);
|
|
}
|
|
return t_report("test_wal");
|
|
}
|