180 lines
6.2 KiB
C
180 lines
6.2 KiB
C
/*
|
|||
|
|
* test_index_stress.c — correctness of the persistent treap index
|
||
|
|
* (Section 9.1's sort order; see CLAUDE.md's "Known performance
|
||
|
|
* characteristics" and BENCH.md for why it replaced a flat array) under
|
||
|
|
* heavy, randomized structural churn, cross-checked against an
|
||
|
|
* independent reference model rather than just "did it not crash."
|
||
|
|
*
|
||
|
|
* This exercises exactly the code path BENCH.md's O(n^2) finding and its
|
||
|
|
* fix live in: upper.c's snapshot_upsert/snapshot_remove and the treap
|
||
|
|
* (split3/merge, node refcounting) beneath them, at a scale (thousands
|
||
|
|
* of entries, non-sequential insertion/deletion order, nested
|
||
|
|
* directories, renames) the other test files don't reach.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include "packfs.h"
|
||
|
|
#include "test_harness.h"
|
||
|
|
|
||
|
|
#define N 8000
|
||
|
|
|
||
|
|
static int g_alive[N];
|
||
|
|
|
||
|
|
static void name_for(int id, char *buf, size_t cap) {
|
||
|
|
/* deliberately nested, to exercise readdir's prefix-range logic at
|
||
|
|
* more than one level, not just a flat root directory */
|
||
|
|
snprintf(buf, cap, "/d%03d/f%06d.dat", id % 50, id);
|
||
|
|
}
|
||
|
|
|
||
|
|
static void shuffle(int *order, int n) {
|
||
|
|
for (int i = n - 1; i > 0; i--) {
|
||
|
|
int j = rand() % (i + 1);
|
||
|
|
int t = order[i]; order[i] = order[j]; order[j] = t;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static void create_one(Vfs *v, int id) {
|
||
|
|
char path[64], parent[16];
|
||
|
|
name_for(id, path, sizeof(path));
|
||
|
|
snprintf(parent, sizeof(parent), "/d%03d", id % 50);
|
||
|
|
int err = 0;
|
||
|
|
if (vfs_stat(v, parent, &(VfsStat){0}) != VFS_OK) vfs_mkdir(v, parent);
|
||
|
|
VfsFile *f = vfs_open(v, path, VFS_O_WRONLY | VFS_O_CREAT | VFS_O_TRUNC, &err);
|
||
|
|
CHECK(f != NULL);
|
||
|
|
if (!f) return;
|
||
|
|
char payload[32];
|
||
|
|
int n = snprintf(payload, sizeof(payload), "id=%d", id);
|
||
|
|
CHECK_EQ_INT(vfs_write(f, payload, (pfs_usize)n), n);
|
||
|
|
vfs_close(f);
|
||
|
|
g_alive[id] = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void delete_one(Vfs *v, int id) {
|
||
|
|
char path[64];
|
||
|
|
name_for(id, path, sizeof(path));
|
||
|
|
CHECK_EQ_INT(vfs_unlink(v, path), VFS_OK);
|
||
|
|
g_alive[id] = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Cross-checks the live tree against g_alive[]: every alive id must
|
||
|
|
* stat successfully with the right content; every dead id must NOENT;
|
||
|
|
* and the total count reachable via nested readdir must match the
|
||
|
|
* reference model's count exactly (not just "at least"). */
|
||
|
|
static void verify_consistency(Vfs *v) {
|
||
|
|
int expected_total = 0;
|
||
|
|
for (int id = 0; id < N; id++) {
|
||
|
|
char path[64];
|
||
|
|
name_for(id, path, sizeof(path));
|
||
|
|
VfsStat st;
|
||
|
|
int rc = vfs_stat(v, path, &st);
|
||
|
|
if (g_alive[id]) {
|
||
|
|
expected_total++;
|
||
|
|
CHECK_EQ_INT(rc, VFS_OK);
|
||
|
|
if (rc == VFS_OK) {
|
||
|
|
CHECK_EQ_INT(st.kind, VFS_KIND_FILE);
|
||
|
|
int err = 0;
|
||
|
|
VfsFile *f = vfs_open(v, path, VFS_O_RDONLY, &err);
|
||
|
|
CHECK(f != NULL);
|
||
|
|
if (f) {
|
||
|
|
char buf[32] = {0}, expect[32];
|
||
|
|
vfs_read(f, buf, sizeof(buf) - 1);
|
||
|
|
snprintf(expect, sizeof(expect), "id=%d", id);
|
||
|
|
CHECK_STR_EQ(buf, expect);
|
||
|
|
vfs_close(f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
CHECK_EQ_INT(rc, VFS_ERR_NOENT);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Full nested readdir sweep: sum of children across every /dNNN that
|
||
|
|
* still has any live file must equal the reference model's count.
|
||
|
|
* This exercises upper_visit_range's prefix pruning at every
|
||
|
|
* directory, not just the root. */
|
||
|
|
int counted = 0;
|
||
|
|
for (int d = 0; d < 50; d++) {
|
||
|
|
char dirpath[16];
|
||
|
|
snprintf(dirpath, sizeof(dirpath), "/d%03d", d);
|
||
|
|
VfsDir dir;
|
||
|
|
if (vfs_readdir(v, dirpath, &dir) == VFS_OK) {
|
||
|
|
counted += (int)dir.count;
|
||
|
|
vfs_dir_free(&dir);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
CHECK_EQ_INT(counted, expected_total);
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void) {
|
||
|
|
srand(20260914);
|
||
|
|
Vfs *v = vfs_new();
|
||
|
|
Backend *mem = backend_mem_new();
|
||
|
|
CHECK_EQ_INT(vfs_mount(v, "/", mem), VFS_OK);
|
||
|
|
|
||
|
|
int order[N];
|
||
|
|
for (int i = 0; i < N; i++) order[i] = i;
|
||
|
|
|
||
|
|
/* round 1: create everything, in randomized (non-sequential) order —
|
||
|
|
* the exact pattern that made the flat array O(n^2); the treap must
|
||
|
|
* both stay correct and (implicitly, via this test completing in
|
||
|
|
* reasonable time under sanitizers) stay fast. */
|
||
|
|
shuffle(order, N);
|
||
|
|
for (int i = 0; i < N; i++) create_one(v, order[i]);
|
||
|
|
verify_consistency(v);
|
||
|
|
|
||
|
|
/* round 2: delete a random 40%, in a different random order */
|
||
|
|
shuffle(order, N);
|
||
|
|
for (int i = 0; i < N * 2 / 5; i++) delete_one(v, order[i]);
|
||
|
|
verify_consistency(v);
|
||
|
|
|
||
|
|
/* round 3: recreate half of what was just deleted, delete a
|
||
|
|
* different random slice of what's still alive, interleaved by
|
||
|
|
* iterating one combined shuffled order over "toggle this id" */
|
||
|
|
shuffle(order, N);
|
||
|
|
for (int i = 0; i < N; i++) {
|
||
|
|
int id = order[i];
|
||
|
|
if (id % 3 == 0) {
|
||
|
|
if (g_alive[id]) delete_one(v, id); else create_one(v, id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
verify_consistency(v);
|
||
|
|
|
||
|
|
/* renames: move a sample of live files to a fresh, previously-unused
|
||
|
|
* path, verify old path gone / new path present with right content */
|
||
|
|
for (int id = 0; id < N; id += 37) {
|
||
|
|
if (!g_alive[id]) continue;
|
||
|
|
char from[64], to[80];
|
||
|
|
name_for(id, from, sizeof(from));
|
||
|
|
snprintf(to, sizeof(to), "%s.moved", from);
|
||
|
|
CHECK_EQ_INT(vfs_rename(v, from, to), VFS_OK);
|
||
|
|
VfsStat st;
|
||
|
|
CHECK_EQ_INT(vfs_stat(v, from, &st), VFS_ERR_NOENT);
|
||
|
|
CHECK_EQ_INT(vfs_stat(v, to, &st), VFS_OK);
|
||
|
|
CHECK_EQ_INT(vfs_rename(v, to, from), VFS_OK); /* move it back so verify_consistency's model still holds */
|
||
|
|
}
|
||
|
|
verify_consistency(v);
|
||
|
|
|
||
|
|
/* final: delete everything still alive, in yet another random order;
|
||
|
|
* the tree must end up empty and every readdir empty. */
|
||
|
|
shuffle(order, N);
|
||
|
|
for (int i = 0; i < N; i++) if (g_alive[order[i]]) delete_one(v, order[i]);
|
||
|
|
verify_consistency(v);
|
||
|
|
for (int d = 0; d < 50; d++) {
|
||
|
|
char dirpath[16];
|
||
|
|
snprintf(dirpath, sizeof(dirpath), "/d%03d", d);
|
||
|
|
VfsDir dir;
|
||
|
|
if (vfs_readdir(v, dirpath, &dir) == VFS_OK) {
|
||
|
|
CHECK_EQ_INT(dir.count, 0);
|
||
|
|
vfs_dir_free(&dir);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
vfs_unmount(v, "/");
|
||
|
|
backend_free(mem);
|
||
|
|
vfs_free(v);
|
||
|
|
TEST_MAIN_END();
|
||
|
|
}
|