fa47b98d18
Self-contained paged store (lang/runtime/engram_store.{c,h}): 16KiB slotted pages,
u32 TLV self-describing records (forward-compatible), overflow chains, B+-tree
id-index + from/to adjacency, page free-list, tombstones, double superblock + crc
recovery. Not yet wired to activation (M3). 33/33 tests pass (ASan/UBSan clean);
5k nodes/20k edges round-trip bit-exact incl 768xf32 emb + hebb; store 25MB vs 64MB
JSON. Format is final — see design §2.4.
440 lines
19 KiB
C
440 lines
19 KiB
C
/* test_store.c — M1 gate for the engram paged store (engram_store.{c,h}).
|
|
*
|
|
* Pure C. Build: gcc -O2 test_store.c ../../lang/runtime/engram_store.c -o test_store
|
|
* Writes ONLY under a throwaway /tmp dir. Never touches ~/.neuron or live ports.
|
|
*
|
|
* Covers §7 M1 gates: round-trip (5k nodes / 20k edges, all fields, emb bit-exact,
|
|
* hebb, >page content), TLV forward-compat, overflow chains, B+-tree indexes
|
|
* across splits, free-list reuse, and corruption/superblock recovery.
|
|
*/
|
|
#include "../../lang/runtime/engram_store.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/stat.h>
|
|
|
|
static int g_pass = 0, g_fail = 0;
|
|
static void ok(const char* name, int cond){
|
|
printf(" [%s] %s\n", cond ? "PASS" : "FAIL", name);
|
|
if (cond) g_pass++; else g_fail++;
|
|
}
|
|
|
|
static char g_dir[512];
|
|
static void mk_dir(void){
|
|
snprintf(g_dir, sizeof g_dir, "/tmp/engram-store-test-%d", (int)getpid());
|
|
mkdir(g_dir, 0700);
|
|
}
|
|
static void path_in(char* out, size_t cap, const char* name){
|
|
snprintf(out, cap, "%s/%s", g_dir, name);
|
|
}
|
|
static long file_size(const char* p){ struct stat st; return stat(p,&st)==0 ? (long)st.st_size : -1; }
|
|
|
|
/* ── deterministic RNG so oracle nodes/edges regenerate bit-exact ─────────── */
|
|
static uint64_t xs(uint64_t* s){ uint64_t x=*s; x^=x<<13; x^=x>>7; x^=x<<17; *s=x; return x; }
|
|
static uint64_t node_seed(int i){ return 0x9E3779B97F4A7C15ULL ^ ((uint64_t)(i+1)*0xD1B54A32D192ED03ULL); }
|
|
static uint64_t edge_seed(int i){ return 0xC2B2AE3D27D4EB4FULL ^ ((uint64_t)(i+1)*0x165667B19E3779F9ULL); }
|
|
|
|
static char* rnd_str(uint64_t* st, size_t len){
|
|
char* s = (char*)malloc(len + 1);
|
|
for (size_t i=0;i<len;i++) s[i] = (char)(33 + (xs(st) % 94)); /* printable, no NUL */
|
|
s[len] = 0; return s;
|
|
}
|
|
|
|
/* NODE_COUNT nodes; a slice have >page content to force overflow chains. */
|
|
#define NODE_COUNT 5000
|
|
#define EDGE_COUNT 20000
|
|
#define EMB_DIM 768
|
|
|
|
static void gen_node(int i, StoreNode* n){
|
|
memset(n, 0, sizeof *n);
|
|
uint64_t st = node_seed(i);
|
|
char id[32]; snprintf(id, sizeof id, "node-%d", i);
|
|
n->id = strdup(id);
|
|
size_t clen = (i % 500 == 0) ? (size_t)(17000 + (xs(&st) % 6000)) : (size_t)(xs(&st) % 300);
|
|
n->content = rnd_str(&st, clen);
|
|
n->node_type = rnd_str(&st, 4 + (xs(&st) % 8));
|
|
n->label = (i % 2) ? rnd_str(&st, 3 + (xs(&st) % 10)) : NULL;
|
|
n->tier = rnd_str(&st, 4 + (xs(&st) % 6));
|
|
n->tags = rnd_str(&st, xs(&st) % 40);
|
|
n->metadata = (i % 3) ? rnd_str(&st, xs(&st) % 60) : NULL;
|
|
n->salience = (double)(xs(&st) % 1000000) / 997.0;
|
|
n->importance = (double)(xs(&st) % 1000000) / 131.0;
|
|
n->confidence = (double)(xs(&st) % 1000000) / 733.0;
|
|
n->temporal_decay_rate = (double)(xs(&st) % 1000000) / 101.0;
|
|
n->activation_count = (int64_t)(xs(&st) % 100000);
|
|
n->last_activated = (int64_t)xs(&st);
|
|
n->created_at = (int64_t)(1600000000000LL + i);
|
|
n->updated_at = (int64_t)xs(&st);
|
|
n->background_activation = (double)(xs(&st) % 1000000) / 17.0;
|
|
n->working_memory_weight = (double)(xs(&st) % 1000000) / 29.0;
|
|
n->suppression_count = (int32_t)(xs(&st) % 50);
|
|
n->layer_id = (uint32_t)(xs(&st) % 5);
|
|
for (int k=0;k<STORE_BLL_K;k++) n->access_ts[k] = (int64_t)xs(&st);
|
|
n->access_head = (int32_t)(xs(&st) % STORE_BLL_K);
|
|
n->access_filled = (int32_t)(xs(&st) % (STORE_BLL_K + 1));
|
|
n->wm_anchor = (double)(xs(&st) % 1000000) / 3.0;
|
|
n->emb = (float*)malloc(EMB_DIM * sizeof(float));
|
|
for (int k=0;k<EMB_DIM;k++){ uint32_t u=(uint32_t)xs(&st); memcpy(&n->emb[k], &u, 4); }
|
|
n->emb_dim = EMB_DIM;
|
|
}
|
|
|
|
static void gen_edge(int i, StoreEdge* e){
|
|
memset(e, 0, sizeof *e);
|
|
uint64_t st = edge_seed(i);
|
|
char id[32], from[32], to[32];
|
|
snprintf(id, sizeof id, "edge-%d", i);
|
|
snprintf(from, sizeof from, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
|
snprintf(to, sizeof to, "node-%d", (int)(xs(&st) % NODE_COUNT));
|
|
e->id = strdup(id); e->from_id = strdup(from); e->to_id = strdup(to);
|
|
e->relation = rnd_str(&st, 3 + (xs(&st) % 12));
|
|
e->metadata = (i % 4) ? rnd_str(&st, xs(&st) % 40) : NULL;
|
|
e->weight = (double)(xs(&st) % 1000000) / 111.0;
|
|
e->hebb = (double)(xs(&st) % 1000000) / 1000000.0; /* the learned field */
|
|
e->confidence = (double)(xs(&st) % 1000000) / 777.0;
|
|
e->created_at = (int64_t)(1600000000000LL + i);
|
|
e->updated_at = (int64_t)xs(&st);
|
|
e->last_fired = (int64_t)xs(&st);
|
|
e->inhibitory = (int32_t)(xs(&st) % 2);
|
|
e->layer_id = (uint32_t)(xs(&st) % 5);
|
|
}
|
|
|
|
static int streq(const char* a, const char* b){
|
|
if (!a && !b) return 1;
|
|
if (!a || !b) return 0;
|
|
return strcmp(a,b)==0;
|
|
}
|
|
static int cmp_node(const StoreNode* a, const StoreNode* b){
|
|
if (!streq(a->id,b->id) || !streq(a->content,b->content) ||
|
|
!streq(a->node_type,b->node_type) || !streq(a->label,b->label) ||
|
|
!streq(a->tier,b->tier) || !streq(a->tags,b->tags) ||
|
|
!streq(a->metadata,b->metadata)) return 0;
|
|
if (a->salience!=b->salience || a->importance!=b->importance ||
|
|
a->confidence!=b->confidence || a->temporal_decay_rate!=b->temporal_decay_rate ||
|
|
a->activation_count!=b->activation_count || a->last_activated!=b->last_activated ||
|
|
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
|
a->background_activation!=b->background_activation ||
|
|
a->working_memory_weight!=b->working_memory_weight ||
|
|
a->suppression_count!=b->suppression_count || a->layer_id!=b->layer_id ||
|
|
a->access_head!=b->access_head || a->access_filled!=b->access_filled ||
|
|
a->wm_anchor!=b->wm_anchor || a->emb_dim!=b->emb_dim) return 0;
|
|
for (int k=0;k<STORE_BLL_K;k++) if (a->access_ts[k]!=b->access_ts[k]) return 0;
|
|
if ((a->emb==NULL) != (b->emb==NULL)) return 0;
|
|
if (a->emb && memcmp(a->emb, b->emb, (size_t)a->emb_dim*4)!=0) return 0;
|
|
return 1;
|
|
}
|
|
static int cmp_edge(const StoreEdge* a, const StoreEdge* b){
|
|
if (!streq(a->id,b->id) || !streq(a->from_id,b->from_id) || !streq(a->to_id,b->to_id) ||
|
|
!streq(a->relation,b->relation) || !streq(a->metadata,b->metadata)) return 0;
|
|
if (a->weight!=b->weight || a->hebb!=b->hebb || a->confidence!=b->confidence ||
|
|
a->created_at!=b->created_at || a->updated_at!=b->updated_at ||
|
|
a->last_fired!=b->last_fired || a->inhibitory!=b->inhibitory ||
|
|
a->layer_id!=b->layer_id) return 0;
|
|
return 1;
|
|
}
|
|
static void free_node_fields(StoreNode* n){
|
|
free(n->id); free(n->content); free(n->node_type); free(n->label);
|
|
free(n->tier); free(n->tags); free(n->metadata); free(n->emb); free(n->unknown);
|
|
}
|
|
static void free_edge_fields(StoreEdge* e){
|
|
free(e->id); free(e->from_id); free(e->to_id); free(e->relation); free(e->metadata); free(e->unknown);
|
|
}
|
|
|
|
/* Flip one byte in the store file at (page*PAGE_SIZE + off). */
|
|
static void flip_byte(const char* path, uint64_t page, size_t off){
|
|
int fd = open(path, O_RDWR);
|
|
uint8_t b; off_t at = (off_t)page*STORE_PAGE_SIZE + off;
|
|
pread(fd, &b, 1, at); b ^= 0xFF; pwrite(fd, &b, 1, at); close(fd);
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════════════════════════ */
|
|
|
|
static void test_roundtrip(void){
|
|
printf("\n== round-trip: %d nodes + %d edges, all fields, emb bit-exact ==\n", NODE_COUNT, EDGE_COUNT);
|
|
char path[600]; path_in(path, sizeof path, "roundtrip.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
ok("store_create", s != NULL);
|
|
if (!s) return;
|
|
|
|
for (int i=0;i<NODE_COUNT;i++){ StoreNode n; gen_node(i,&n);
|
|
if (store_put_node(s,&n)!=0){ ok("put_node", 0); free_node_fields(&n); store_close(s); return; }
|
|
free_node_fields(&n); }
|
|
for (int i=0;i<EDGE_COUNT;i++){ StoreEdge e; gen_edge(i,&e);
|
|
if (store_put_edge(s,&e)!=0){ ok("put_edge", 0); free_edge_fields(&e); store_close(s); return; }
|
|
free_edge_fields(&e); }
|
|
ok("wrote all nodes+edges", 1);
|
|
store_close(s);
|
|
|
|
long sz = file_size(path);
|
|
printf(" store file size: %ld bytes (%.2f MB) for %d nodes / %d edges\n",
|
|
sz, sz/1048576.0, NODE_COUNT, EDGE_COUNT);
|
|
|
|
s = store_open(path);
|
|
ok("store_open (reopen)", s != NULL);
|
|
if (!s) return;
|
|
|
|
int nbad = 0;
|
|
for (int i=0;i<NODE_COUNT;i++){
|
|
StoreNode want; gen_node(i,&want);
|
|
StoreNode got; int r = store_get_node(s, want.id, &got);
|
|
if (r!=1 || !cmp_node(&want,&got) || got.unknown_len!=0) nbad++;
|
|
if (r==1) store_node_free(&got);
|
|
free_node_fields(&want);
|
|
}
|
|
ok("all 5000 nodes read back bit-exact (incl emb, all fields)", nbad==0);
|
|
if (nbad) printf(" %d node mismatches\n", nbad);
|
|
|
|
int ebad = 0;
|
|
for (int i=0;i<EDGE_COUNT;i++){
|
|
StoreEdge want; gen_edge(i,&want);
|
|
StoreEdge* got; size_t gn;
|
|
int found = 0;
|
|
if (store_get_edges_from(s, want.from_id, &got, &gn)==0){
|
|
for (size_t j=0;j<gn;j++) if (streq(got[j].id, want.id)){ if (cmp_edge(&want,&got[j])) found=1; break; }
|
|
store_edges_free(got, gn);
|
|
}
|
|
if (!found) ebad++;
|
|
free_edge_fields(&want);
|
|
}
|
|
ok("all 20000 edges read back via adjacency, all fields incl hebb", ebad==0);
|
|
if (ebad) printf(" %d edge mismatches\n", ebad);
|
|
|
|
ok("store_check crc clean after round-trip", store_check(s, STORE_CHECK_CRC)==0);
|
|
store_close(s);
|
|
}
|
|
|
|
static void test_forward_compat(void){
|
|
printf("\n== TLV forward-compat: omit field defaults; unknown tag preserved ==\n");
|
|
char path[600]; path_in(path, sizeof path, "fwd.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
|
|
/* Writer OMITS several fields (metadata, label, emb) → reader must default. */
|
|
StoreNode a; memset(&a,0,sizeof a);
|
|
a.id = strdup("omit-1"); a.content = strdup("has content"); a.tier = strdup("core");
|
|
a.salience = 0.5; /* metadata/label NULL, emb NULL */
|
|
store_put_node(s, &a); free(a.id); free(a.content); free(a.tier);
|
|
|
|
StoreNode g; int r = store_get_node(s, "omit-1", &g);
|
|
ok("omitted string fields default to NULL", r==1 && g.metadata==NULL && g.label==NULL);
|
|
ok("omitted emb defaults to NULL / emb_dim 0", r==1 && g.emb==NULL && g.emb_dim==0);
|
|
ok("present fields intact", r==1 && streq(g.content,"has content") && g.salience==0.5);
|
|
if (r==1) store_node_free(&g);
|
|
|
|
/* Writer includes an UNKNOWN tag (simulating a newer writer / field the
|
|
* reader does not model) via the `unknown` passthrough. Reader (which also
|
|
* models known fields A,B,C) must preserve it verbatim. */
|
|
uint8_t unk[64];
|
|
unk[0] = 200; /* a tag this build has no case for */
|
|
/* [u8 tag][u32 len][bytes] */
|
|
unk[1]=8; unk[2]=0; unk[3]=0; unk[4]=0;
|
|
for (int i=0;i<8;i++) unk[5+i] = (uint8_t)(0xA0 + i);
|
|
StoreNode b; memset(&b,0,sizeof b);
|
|
b.id = strdup("unk-1"); b.content = strdup("known field B"); b.confidence = 0.9; /* known field C-ish */
|
|
b.unknown = unk; b.unknown_len = 5 + 8;
|
|
store_put_node(s, &b); free(b.id); free(b.content);
|
|
|
|
StoreNode g2; int r2 = store_get_node(s, "unk-1", &g2);
|
|
int unk_ok = r2==1 && g2.unknown_len==(5+8) && memcmp(g2.unknown, unk, 5+8)==0;
|
|
ok("unknown tag preserved verbatim on read", unk_ok);
|
|
ok("known fields still read while unknown preserved", r2==1 && streq(g2.content,"known field B") && g2.confidence==0.9);
|
|
if (r2==1) store_node_free(&g2);
|
|
|
|
store_close(s);
|
|
}
|
|
|
|
static void test_overflow(void){
|
|
printf("\n== overflow: 100KB content node + emb via overflow chain ==\n");
|
|
char path[600]; path_in(path, sizeof path, "ovf.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
|
|
size_t big = 100*1024;
|
|
StoreNode n; memset(&n,0,sizeof n);
|
|
n.id = strdup("big-1");
|
|
n.content = (char*)malloc(big+1);
|
|
for (size_t i=0;i<big;i++) n.content[i] = (char)(33 + (i % 94));
|
|
n.content[big] = 0;
|
|
n.tier = strdup("episodic");
|
|
n.emb = (float*)malloc(EMB_DIM*sizeof(float));
|
|
for (int k=0;k<EMB_DIM;k++){ float f = (float)(k*0.5 - 100.0); n.emb[k]=f; }
|
|
n.emb_dim = EMB_DIM;
|
|
ok("put 100KB+emb node", store_put_node(s,&n)==0);
|
|
store_close(s);
|
|
|
|
s = store_open(path);
|
|
StoreNode g; int r = store_get_node(s, "big-1", &g);
|
|
ok("reopen + read big node", r==1);
|
|
ok("100KB content byte-exact via overflow", r==1 && strlen(g.content)==big && memcmp(g.content,n.content,big)==0);
|
|
ok("emb bit-exact via overflow record", r==1 && g.emb_dim==EMB_DIM && memcmp(g.emb,n.emb,EMB_DIM*4)==0);
|
|
if (r==1) store_node_free(&g);
|
|
ok("store_check clean (overflow pages crc'd)", store_check(s, STORE_CHECK_CRC)==0);
|
|
store_close(s);
|
|
free_node_fields(&n);
|
|
}
|
|
|
|
static void test_index_splits(void){
|
|
printf("\n== B+-tree index correctness across many splits ==\n");
|
|
char path[600]; path_in(path, sizeof path, "idx.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
/* Tiny order forces deep leaf + internal splits with only a few hundred keys. */
|
|
store__set_btree_order(s, 4, 4);
|
|
|
|
const int N = 600;
|
|
for (int i=0;i<N;i++){
|
|
StoreNode n; memset(&n,0,sizeof n);
|
|
char id[32]; snprintf(id,sizeof id,"k-%05d", (i*37+11)%100000); /* scattered keys */
|
|
n.id = strdup(id); n.content = strdup("x"); n.tier=strdup("t"); n.salience=i;
|
|
if (store_put_node(s,&n)!=0){ ok("put",0); }
|
|
free(n.id); free(n.content); free(n.tier);
|
|
}
|
|
int miss=0;
|
|
for (int i=0;i<N;i++){
|
|
char id[32]; snprintf(id,sizeof id,"k-%05d",(i*37+11)%100000);
|
|
StoreNode g; int r = store_get_node(s, id, &g);
|
|
if (r!=1 || (int)g.salience != i) miss++;
|
|
if (r==1) store_node_free(&g);
|
|
}
|
|
ok("all keys retrievable after leaf+internal splits", miss==0);
|
|
if (miss) printf(" %d misses\n", miss);
|
|
StoreNode g; ok("absent key returns 0", store_get_node(s,"k-NOPE",&g)==0);
|
|
|
|
/* Adjacency: controlled star + chain, exact edge sets. */
|
|
for (int i=0;i<50;i++){
|
|
StoreEdge e; memset(&e,0,sizeof e);
|
|
char id[32]; snprintf(id,sizeof id,"e-%d",i);
|
|
e.id=strdup(id); e.from_id=strdup("HUB"); char tt[16]; snprintf(tt,sizeof tt,"T-%d",i); e.to_id=strdup(tt);
|
|
e.relation=strdup("r"); e.weight=1.0; e.hebb=0.1*i;
|
|
store_put_edge(s,&e); free_edge_fields(&e);
|
|
}
|
|
for (int i=0;i<7;i++){
|
|
StoreEdge e; memset(&e,0,sizeof e);
|
|
char id[32]; snprintf(id,sizeof id,"in-%d",i);
|
|
char ff[16]; snprintf(ff,sizeof ff,"S-%d",i);
|
|
e.id=strdup(id); e.from_id=strdup(ff); e.to_id=strdup("SINK");
|
|
e.relation=strdup("r"); e.weight=1.0;
|
|
store_put_edge(s,&e); free_edge_fields(&e);
|
|
}
|
|
StoreEdge* out; size_t on;
|
|
store_get_edges_from(s,"HUB",&out,&on);
|
|
ok("get_edges_from(HUB) == 50", on==50);
|
|
store_edges_free(out,on);
|
|
store_get_edges_to(s,"SINK",&out,&on);
|
|
ok("get_edges_to(SINK) == 7", on==7);
|
|
store_edges_free(out,on);
|
|
store_get_edges_to(s,"HUB",&out,&on);
|
|
ok("get_edges_to(HUB) == 0 (direction separation)", on==0);
|
|
store_edges_free(out,on);
|
|
|
|
ok("store_check clean", store_check(s, STORE_CHECK_CRC)==0);
|
|
store_close(s);
|
|
}
|
|
|
|
static void test_freelist(void){
|
|
printf("\n== free-list: tombstone reclaims pages, graph stays consistent ==\n");
|
|
char path[600]; path_in(path, sizeof path, "free.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
|
|
uint64_t pc0 = store_page_count(s);
|
|
const int N = 300;
|
|
for (int i=0;i<N;i++){
|
|
StoreNode n; memset(&n,0,sizeof n);
|
|
char id[32]; snprintf(id,sizeof id,"a-%d",i);
|
|
n.id=strdup(id); n.content=rnd_str(&(uint64_t){node_seed(i)}, 200); n.tier=strdup("t");
|
|
store_put_node(s,&n); free_node_fields(&n);
|
|
}
|
|
uint64_t pc1 = store_page_count(s);
|
|
uint64_t node_pages = pc1 - pc0;
|
|
ok("initial batch consumed pages", node_pages > 0);
|
|
|
|
for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i); store_tombstone(s,id); }
|
|
/* all old nodes gone */
|
|
int gone=1; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"a-%d",i);
|
|
StoreNode g; if (store_get_node(s,id,&g)==1){ gone=0; store_node_free(&g); } }
|
|
ok("tombstoned nodes now absent", gone);
|
|
|
|
for (int i=0;i<N;i++){
|
|
StoreNode n; memset(&n,0,sizeof n);
|
|
char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
|
n.id=strdup(id); n.content=strdup("reused"); n.tier=strdup("t"); n.salience=i;
|
|
store_put_node(s,&n); free_node_fields(&n);
|
|
}
|
|
uint64_t pc2 = store_page_count(s);
|
|
/* Reuse proven: growth for the 2nd batch is far less than a fresh alloc. */
|
|
ok("freed pages reused (no full re-growth)", pc2 < pc1 + node_pages);
|
|
printf(" pages: base=%llu after1=%llu after2=%llu (node_pages=%llu)\n",
|
|
(unsigned long long)pc0,(unsigned long long)pc1,(unsigned long long)pc2,(unsigned long long)node_pages);
|
|
|
|
int newbad=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
|
StoreNode g; if (store_get_node(s,id,&g)!=1 || (int)g.salience!=i) newbad++; else store_node_free(&g); }
|
|
ok("new batch fully readable after reuse", newbad==0);
|
|
ok("store_check clean after reuse", store_check(s, STORE_CHECK_CRC)==0);
|
|
|
|
store_close(s);
|
|
/* survives reopen */
|
|
s = store_open(path);
|
|
int rb=0; for (int i=0;i<N;i++){ char id[32]; snprintf(id,sizeof id,"b-%d",i);
|
|
StoreNode g; if (store_get_node(s,id,&g)!=1) rb++; else store_node_free(&g); }
|
|
ok("graph consistent across reopen after reuse", rb==0);
|
|
store_close(s);
|
|
}
|
|
|
|
static void test_corruption(void){
|
|
printf("\n== corruption: crc detection + superblock mirror recovery ==\n");
|
|
char path[600]; path_in(path, sizeof path, "corrupt.store");
|
|
unlink(path);
|
|
EngramPagedStore* s = store_create(path);
|
|
for (int i=0;i<50;i++){ StoreNode n; gen_node(i,&n); store_put_node(s,&n); free_node_fields(&n); }
|
|
store_close(s);
|
|
|
|
s = store_open(path);
|
|
ok("clean store: store_check == 0", store_check(s, STORE_CHECK_CRC)==0);
|
|
store_close(s);
|
|
|
|
/* flip a byte inside a data page (page 5 is node/index data, never a SB) */
|
|
flip_byte(path, 5, 137);
|
|
s = store_open(path);
|
|
ok("store_open still succeeds (data-page corruption)", s != NULL);
|
|
int bad = store_check(s, STORE_CHECK_CRC);
|
|
ok("store_check detects corrupted page via crc", bad >= 1);
|
|
printf(" store_check reported %d corrupt page(s)\n", bad);
|
|
store_close(s);
|
|
|
|
/* fresh store, corrupt superblock 0, must recover via mirror superblock 1 */
|
|
char p2[600]; path_in(p2, sizeof p2, "sbrec.store");
|
|
unlink(p2);
|
|
s = store_create(p2);
|
|
StoreNode n; gen_node(42,&n); store_put_node(s,&n);
|
|
store_close(s);
|
|
/* trash magic + crc region of page 0 */
|
|
flip_byte(p2, 0, 0); flip_byte(p2, 0, 1); flip_byte(p2, 0, 90);
|
|
s = store_open(p2);
|
|
ok("open recovers via mirror superblock (page 1)", s != NULL);
|
|
if (s){
|
|
StoreNode g; int r = store_get_node(s, "node-42", &g);
|
|
ok("data intact after superblock recovery", r==1 && cmp_node(&n,&g));
|
|
if (r==1) store_node_free(&g);
|
|
store_close(s);
|
|
}
|
|
free_node_fields(&n);
|
|
}
|
|
|
|
int main(void){
|
|
mk_dir();
|
|
printf("engram_store M1 test harness — dir=%s\n", g_dir);
|
|
test_roundtrip();
|
|
test_forward_compat();
|
|
test_overflow();
|
|
test_index_splits();
|
|
test_freelist();
|
|
test_corruption();
|
|
printf("\n================ %d passed, %d failed ================\n", g_pass, g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|