swactor/crates/wasm/test.mjs
zacheryasc 18cabdb94d feat: skeleton for in browser swactor engine (#35)
Skeleton up for a web browser swactor engine that is capable of connecting with a cluster.


Signed-off-by: Zachery Aaron Shores-Chmielewski <zacheryasc@gmail.com>
2026-02-13 13:27:34 +00:00

346 lines
10 KiB
JavaScript

import {
WasmRuntime,
WasmAddr,
spawn_counter,
spawn_relay,
spawn_sentinel,
spawn_group_member,
} from "./pkg/wasm.js";
let passed = 0;
let failed = 0;
function assert(cond, msg) {
if (!cond) {
console.error(` FAIL: ${msg}`);
failed++;
} else {
passed++;
}
}
function assertEq(a, b, msg) {
const aj = JSON.stringify(a);
const bj = JSON.stringify(b);
if (aj !== bj) {
console.error(` FAIL: ${msg} (got ${aj}, expected ${bj})`);
failed++;
} else {
passed++;
}
}
function drainInbox(inbox) {
const results = [];
let v;
while ((v = inbox.try_recv()) !== undefined) results.push(v);
return results;
}
// ---- accumulator ----------------------------------------------------------
{
console.log("test: accumulator processes messages via WasmAddr");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
rt.send_u32(c, 1);
rt.send_u32(c, 2);
rt.send_u32(c, 10);
rt.tick();
assertEq(drainInbox(inbox), [1, 3, 13], "running totals");
inbox.free();
rt.free();
}
// ---- relay ----------------------------------------------------------------
{
console.log("test: relay forwards to counter");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
const r = spawn_relay(rt, c);
rt.send_u32(r, 5);
rt.send_u32(r, 7);
// tick 1: relay receives and forwards (same worker → pending_local)
// tick 2: counter receives forwarded messages
rt.tick();
rt.tick();
assertEq(drainInbox(inbox), [5, 12], "relayed totals");
inbox.free();
rt.free();
}
// ---- multiple counters ----------------------------------------------------
{
console.log("test: multiple independent counters");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const a = spawn_counter(rt, inbox.addr());
const b = spawn_counter(rt, inbox.addr());
rt.send_u32(a, 10);
rt.send_u32(b, 100);
rt.tick();
const results = drainInbox(inbox);
assert(
results.includes(10) && results.includes(100) && results.length === 2,
"both counters report"
);
inbox.free();
rt.free();
}
// ---- actor_count ----------------------------------------------------------
{
console.log("test: actor_count tracks spawns");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
spawn_counter(rt, inbox.addr());
spawn_counter(rt, inbox.addr());
spawn_counter(rt, inbox.addr());
rt.tick(); // drain spawn queue
assertEq(rt.actor_count(), 3, "three actors");
inbox.free();
rt.free();
}
// ---- WasmAddr toString ----------------------------------------------------
{
console.log("test: WasmAddr has string representation");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const addr = spawn_counter(rt, inbox.addr());
const s = addr.toString();
// no_random generates deterministic addresses — just check it's a non-empty hex string
assert(typeof s === "string" && s.length > 0, "addr toString is non-empty string");
inbox.free();
rt.free();
}
// ---- stop_actor -----------------------------------------------------------
{
console.log("test: stop_actor removes actor");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
rt.tick(); // drain spawn
assertEq(rt.actor_count(), 1, "one actor before stop");
rt.stop_actor(c);
rt.tick(); // process stop + cleanup
assertEq(rt.actor_count(), 0, "zero actors after stop");
inbox.free();
rt.free();
}
// ---- bytes inbox ----------------------------------------------------------
{
console.log("test: byte inbox receives Uint8Array");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_bytes();
// Send bytes directly (no actor — just to the inbox address)
rt.send_bytes(inbox.addr(), new Uint8Array([1, 2, 3]));
rt.tick();
const result = inbox.try_recv();
assert(result instanceof Uint8Array, "result is Uint8Array");
assertEq(Array.from(result), [1, 2, 3], "bytes match");
inbox.free();
rt.free();
}
// ---- uptime ---------------------------------------------------------------
{
console.log("test: uptime_ms returns a number");
const rt = new WasmRuntime();
const uptime = rt.uptime_ms();
assert(typeof uptime === "number" && uptime >= 0, "uptime is non-negative number");
rt.free();
}
// ---- naming: register and resolve -----------------------------------------
{
console.log("test: naming — register_name and where_is");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
rt.tick(); // drain spawn
const ok = rt.register_name("my_counter", c);
assert(ok, "register_name succeeds");
const found = rt.where_is("my_counter");
assert(found !== undefined, "where_is finds registered actor");
assertEq(found.toString(), c.toString(), "where_is returns correct address");
const notFound = rt.where_is("nonexistent");
assert(notFound === undefined, "where_is returns undefined for unknown name");
found.free();
inbox.free();
rt.free();
}
// ---- naming: unregister ---------------------------------------------------
{
console.log("test: naming — unregister_name");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
rt.tick();
rt.register_name("temp", c);
const prev = rt.unregister_name("temp");
assert(prev !== undefined, "unregister returns previous address");
assertEq(prev.toString(), c.toString(), "unregister returns correct address");
const gone = rt.where_is("temp");
assert(gone === undefined, "name no longer resolves after unregister");
prev.free();
inbox.free();
rt.free();
}
// ---- naming: registered_names ---------------------------------------------
{
console.log("test: naming — registered_names");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const a = spawn_counter(rt, inbox.addr());
const b = spawn_counter(rt, inbox.addr());
rt.tick();
rt.register_name("alpha", a);
rt.register_name("beta", b);
const names = rt.registered_names().split(",").sort();
assertEq(names, ["alpha", "beta"], "registered_names lists all names");
inbox.free();
rt.free();
}
// ---- naming: duplicate name rejected --------------------------------------
{
console.log("test: naming — duplicate name rejected");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const a = spawn_counter(rt, inbox.addr());
const b = spawn_counter(rt, inbox.addr());
rt.tick();
const ok1 = rt.register_name("unique", a);
const ok2 = rt.register_name("unique", b);
assert(ok1, "first registration succeeds");
assert(!ok2, "duplicate registration fails");
inbox.free();
rt.free();
}
// ---- groups: join and broadcast -------------------------------------------
{
console.log("test: groups — join_group and publish_to_group_u32");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const a = spawn_group_member(rt, "workers", inbox.addr());
const b = spawn_group_member(rt, "workers", inbox.addr());
rt.tick(); // spawn + on_start (join group)
assertEq(rt.group_member_count("workers"), 2, "two members in group");
rt.publish_to_group_u32("workers", 42);
rt.tick(); // group members receive
rt.tick(); // group members forward to inbox
const results = drainInbox(inbox);
assertEq(results.length, 2, "both members received broadcast");
assert(results.every((v) => v === 42), "correct value broadcast");
inbox.free();
rt.free();
}
// ---- groups: leave --------------------------------------------------------
{
console.log("test: groups — leave_group");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const a = spawn_group_member(rt, "pool", inbox.addr());
const b = spawn_group_member(rt, "pool", inbox.addr());
rt.tick(); // spawn + on_start
assertEq(rt.group_member_count("pool"), 2, "two members before leave");
rt.leave_group(a, "pool");
assertEq(rt.group_member_count("pool"), 1, "one member after leave");
inbox.free();
rt.free();
}
// ---- groups: group_names --------------------------------------------------
{
console.log("test: groups — group_names");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
spawn_group_member(rt, "alpha", inbox.addr());
spawn_group_member(rt, "beta", inbox.addr());
rt.tick(); // spawn + join
const names = rt.group_names().split(",").sort();
assertEq(names, ["alpha", "beta"], "group_names lists all groups");
inbox.free();
rt.free();
}
// ---- watching: sentinel detects death -------------------------------------
{
console.log("test: watching — sentinel reports actor death");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const deathInbox = rt.new_inbox_string();
const target = spawn_counter(rt, inbox.addr());
const sentinel = spawn_sentinel(rt, target, deathInbox);
rt.tick(); // spawn + on_start (watch)
rt.stop_actor(target);
// tick to process stop, cleanup, and deliver death notification
for (let i = 0; i < 5; i++) rt.tick();
const notification = deathInbox.try_recv();
assert(notification !== undefined, "sentinel received death notification");
assert(
typeof notification === "string" && notification.length > 0,
"notification is a non-empty string"
);
inbox.free();
deathInbox.free();
rt.free();
}
// ---- stats: total_messages ------------------------------------------------
{
console.log("test: stats — total_messages");
const rt = new WasmRuntime();
const inbox = rt.new_inbox_u32();
const c = spawn_counter(rt, inbox.addr());
rt.send_u32(c, 1);
rt.send_u32(c, 2);
rt.send_u32(c, 3);
rt.tick();
assert(rt.total_messages() >= 3, "total_messages counts processed messages");
inbox.free();
rt.free();
}
// ---- stats: total_panics starts at zero -----------------------------------
{
console.log("test: stats — total_panics starts at zero");
const rt = new WasmRuntime();
assertEq(rt.total_panics(), 0, "no panics initially");
rt.free();
}
// ---- results --------------------------------------------------------------
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);