writeonce/runtime/test/test_loader.c
shoney.arickathil d38b4f864b feat(db2-ephemeral): refuse a durable table without WO_DATA; WO_EPHEMERAL=1 opts out; .wob v8 table bit
- main.c, startup only: WO_DATA unset and a class carrying WO_CLASSF_TABLE
  without WO_CLASSF_VOLATILE (`durable: true`, the default) refuses — exit 2,
  one stderr line naming the class and the three ways forward (WO_DATA=<dir
  or file>, WO_EPHEMERAL=1, @table(durable: false)); before, every write
  was silently dropped at exit — the one outcome `durable: true` forbids
- WO_EPHEMERAL=1 (exact value) is the whole-program escape: one boot notice,
  rc 0, the RAM path byte-for-byte the old one (db.c untouched); any other
  value refuses; set alongside WO_DATA refuses regardless of tables; the
  `resident: keys` loop still wins and is not rescued
- .wob v8: WO_CLASSF_TABLE 0x08 (WO_CLASSF_ALL 0x0f), set from emit.ml's
  cr_is_table — the first cut keyed on !VOLATILE and refused every
  class-bearing program (fibers' Tick, subprocess's ConnMsg), because v7
  spelled `durable: true` as the mere absence of a bit
- loader refuses VOLATILE/RESIDENT_KEYS without the table bit ("storage
  flags on a class that is not a @table"); a v7 image is refused by the
  exact-match version check, as v7 refused v6; disasm prints `table`;
  runner.ml's independent validator carries both rules; obj.h comment
- test_loader: test_storage_flags_need_table (forged flags word: both
  refusals, and the same bits WITH the table bit load); no golden moved
- contract: docs/plan/oop-vm/00-wob-format.md "v8: the table bit"

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 863692a590d426da0047831ae315142ef5b24416)
2026-09-15 01:16:13 +02:00

152 lines
4.9 KiB
C

/* test_loader — happy path from a builder image, then a rejection battery.
* Every reject must produce a nonempty error and leak nothing (ASan). */
#include <stdlib.h>
#include "loader.h"
#include "t.h"
#include "wob_build.h"
/* a valid two-const, one-class, one-method image */
static uint8_t *valid_image(size_t *len) {
wb_t *b = wb_new();
uint32_t k42 = wb_const_int(b, 42);
uint32_t kname = wb_const_text(b, "main");
uint8_t kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
wb_class(b, kname, 0, kinds, 2);
uint32_t code[] = {
wo_ins_abx(WOP_LOADK, 0, (uint16_t)k42),
wo_ins_abc(WOP_RET, 0, 0, 0),
};
uint32_t lines[] = {0, 10, 1, 11};
uint32_t m = wb_method(b, kname, WOB_NONE, 0, 2, code, 2, lines, 2, NULL, 0);
wb_entry(b, m);
return wb_finish(b, len);
}
static void test_happy_path(void) {
size_t len;
uint8_t *img = valid_image(&len);
wo_module m;
char err[256] = "";
T_EQ(wo_load_buf(&m, img, len, err, sizeof err), 0);
T_EQ(m.const_cnt, 2);
T_EQ(m.consts[0].tag, WOB_K_INT);
T_EQ(m.consts[0].i, 42);
T_EQ(m.consts[1].tag, WOB_K_TEXT);
T_CHECK(m.consts[1].s != NULL);
T_CHECK(m.consts[1].s->h.flags & WO_F_CONST); /* interned */
T_EQ(m.consts[1].s->len, 4);
T_CHECK(memcmp(m.consts[1].s->data, "main", 4) == 0);
T_EQ(m.class_cnt, 1);
T_EQ(m.classes[0].field_cnt, 2);
T_EQ(m.method_cnt, 1);
T_EQ(m.methods[0].reg_cnt, 2);
T_EQ(m.methods[0].ninstr, 2);
T_EQ(m.methods[0].line_cnt, 2);
T_EQ(m.methods[0].lines[1].pc, 1);
T_EQ(m.methods[0].lines[1].line, 11);
T_EQ(m.entry, 0);
wo_module_free(&m);
free(img);
}
/* build an image with one patched method-code instruction */
static uint8_t *image_with_code(uint32_t i0, uint32_t i1, size_t *len) {
wb_t *b = wb_new();
wb_const_int(b, 42);
uint32_t kname = wb_const_text(b, "main");
uint32_t code[] = {i0, i1};
wb_method(b, kname, WOB_NONE, 0, 2, code, 2, NULL, 0, NULL, 0);
return wb_finish(b, len);
}
static void expect_reject(uint8_t *img, size_t len, const char *what) {
wo_module m;
char err[256] = "";
int rc = wo_load_buf(&m, img, len, err, sizeof err);
T_EQ(rc, -1);
if (rc != 0 && err[0] == '\0')
fprintf(stderr, "empty error for reject case: %s\n", what);
T_CHECK(err[0] != '\0');
}
static void test_rejects(void) {
size_t len;
/* corrupted magic */
uint8_t *img = valid_image(&len);
img[0] ^= 0xFF;
expect_reject(img, len, "magic");
free(img);
/* unknown opcode */
img = image_with_code(wo_ins_abc(200, 0, 0, 0),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "opcode");
free(img);
/* constant index out of range */
img = image_with_code(wo_ins_abx(WOP_LOADK, 0, 99),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "const oob");
free(img);
/* register out of range (regc = 2) */
img = image_with_code(wo_ins_abc(WOP_MOVE, 5, 0, 0),
wo_ins_abc(WOP_RET, 0, 0, 0), &len);
expect_reject(img, len, "reg oob");
free(img);
/* last instruction is not a terminator */
img = image_with_code(wo_ins_abc(WOP_RET, 0, 0, 0),
wo_ins_abx(WOP_LOADK, 0, 0), &len);
expect_reject(img, len, "non-terminator tail");
free(img);
/* truncated buffer */
img = valid_image(&len);
expect_reject(img, len / 2, "truncated");
free(img);
}
/* a valid one-class image whose class descriptor carries `flags` */
static uint8_t *image_with_class_flags(uint32_t flags, size_t *len) {
wb_t *b = wb_new();
wb_const_int(b, 42);
uint32_t kname = wb_const_text(b, "main");
uint8_t kinds[] = {WO_K_SCALAR, WO_K_SCALAR};
wb_class(b, kname, flags, kinds, 2);
uint32_t code[] = {wo_ins_abc(WOP_RET, 0, 0, 0)};
wb_method(b, kname, WOB_NONE, 0, 1, code, 1, NULL, 0, NULL, 0);
return wb_finish(b, len);
}
/* databasev2 2 (6a, .wob v8): the storage bits describe a @table's rows, so
* they are meaningless — and refused — on a class without WO_CLASSF_TABLE.
* woc never emits the combination; the loader refuses it independently. */
static void test_storage_flags_need_table(void) {
size_t len;
uint8_t *img = image_with_class_flags(WO_CLASSF_VOLATILE, &len);
expect_reject(img, len, "volatile without table");
free(img);
img = image_with_class_flags(WO_CLASSF_RESIDENT_KEYS, &len);
expect_reject(img, len, "resident:keys without table");
free(img);
/* the same bits WITH the table bit load */
img = image_with_class_flags(WO_CLASSF_TABLE | WO_CLASSF_VOLATILE, &len);
wo_module m;
char err[256] = "";
T_EQ(wo_load_buf(&m, img, len, err, sizeof err), 0);
T_EQ(m.classes[0].flags, WO_CLASSF_TABLE | WO_CLASSF_VOLATILE);
wo_module_free(&m);
free(img);
}
int main(void) {
test_happy_path();
test_rejects();
test_storage_flags_need_table();
return t_report("test_loader");
}