#include "ScriptTest.h" #include "zeroerr/assert.h" #include "zeroerr/unittest.h" static const char* kScriptEcsContent = R"SQ( function testEcsReady() { return eve.ecsReady() } function testDefineClassAndComponent() { class Position extends eve.Component { x = 0.0 y = 0.0 } class Velocity extends eve.Component { x = 1.0 y = 2.0 } class Moveable extends eve.Entity { pos = Position vel = Velocity } local e = Moveable.create() if (e == null) return false if (!e.isAlive()) return false if (e.getId() <= 0) return false if (e.pos.x != 0.0 || e.pos.y != 0.0) return false if (e.vel.x != 1.0 || e.vel.y != 2.0) return false e.pos.x = 10.0 e.vel.x = 3.0 if (e.pos.x != 10.0) return false if (e.getComponent(Position).x != 10.0) return false if (!e.hasComponent(Velocity)) return false return true } function testViewIncludesSubclass() { class Health extends eve.Component { hp = 100 } class Actor extends eve.Entity { hp = Health } class Enemy extends Actor { } local a = Actor.create() local b = Enemy.create() a.hp.hp = 50 b.hp.hp = 80 local actors = eve.view(Actor) if (actors.len() < 2) return false local enemies = eve.view(Enemy) if (enemies.len() < 1) return false local foundEnemy = false foreach (e in actors) { if (e == b) foundEnemy = true } if (!foundEnemy) return false return true } function testSystemUpdate() { class Position extends eve.Component { x = 0.0 } class Velocity extends eve.Component { x = 5.0 } class Mover extends eve.Entity { pos = Position vel = Velocity } class MoveSys extends eve.System { constructor() { base.constructor(Mover) } function update(dt) { foreach (e in entities()) { e.pos.x += e.vel.x * dt } } } local e = Mover.create() local sys = MoveSys() sys.update(2.0) if (e.pos.x != 10.0) return false e.destroy() if (e.isAlive()) return false // destroyed entities leave the view foreach (ent in eve.view(Mover)) { if (ent == e) return false } return true } function testSystemDynamicSpawnDespawn() { class Position extends eve.Component { x = 0.0 } class Velocity extends eve.Component { x = 1.0 } class Mover extends eve.Entity { pos = Position vel = Velocity } class MoveSys extends eve.System { constructor() { base.constructor(Mover) } function update(dt) { local dying = [] foreach (e in entities()) { e.pos.x += e.vel.x * dt if (e.pos.x >= 2.0) dying.push(e) } foreach (e in dying) e.destroy() } } local sys = MoveSys() local a = Mover.create() local b = Mover.create() a.vel.x = 1.0 b.vel.x = 1.0 if (eve.view(Mover).len() != 2) return false sys.update(1.0) if (a.pos.x != 1.0 || b.pos.x != 1.0) return false if (eve.view(Mover).len() != 2) return false sys.update(1.0) if (a.isAlive() || b.isAlive()) return false if (eve.view(Mover).len() != 0) return false local c = Mover.create() c.vel.x = 1.0 sys.update(0.5) if (!c.isAlive() || c.pos.x != 0.5) return false if (eve.view(Mover).len() != 1) return false // A later entities() call in the same update sees newly created instances. class SpawnSys extends eve.System { snapshot = 0 constructor() { base.constructor(Mover) } function update(dt) { snapshot = entities().len() local extra = Mover.create() extra.pos.x = 99.0 if (entities().len() != snapshot + 1) return false foreach (e in entities()) { if (e.pos.x >= 2.0) e.destroy() } return true } } local spawnSys = SpawnSys() if (!spawnSys.update(0.0)) return false if (spawnSys.snapshot != 1) return false if (eve.view(Mover).len() != 1) return false foreach (e in eve.view(Mover)) { if (e != c) return false e.destroy() } return eve.view(Mover).len() == 0 } function testSystemQueryArrayAndSubclass() { class Hp extends eve.Component { v = 1 } class Actor extends eve.Entity { hp = Hp } class Enemy extends Actor {} class Prop extends eve.Entity { hp = Hp } class TickSys extends eve.System { constructor() { base.constructor([Actor, Prop]) } function update(dt) { foreach (e in entities()) e.hp.v += 1 } } local actor = Actor.create() local enemy = Enemy.create() local prop = Prop.create() local sys = TickSys() sys.update(0.0) if (actor.hp.v != 2 || enemy.hp.v != 2 || prop.hp.v != 2) return false sys.setQuery(Enemy) sys.update(0.0) if (actor.hp.v != 2 || enemy.hp.v != 3 || prop.hp.v != 2) return false local emptySys = eve.System() if (emptySys.entities().len() != 0) return false actor.destroy() enemy.destroy() prop.destroy() return true } function testTypeMarkersAndEntityContainer() { class Status extends eve.Component { alive = eve.Boolean name = eve.String score = eve.Number } class Thing extends eve.EntityContainer { status = Status } local t = Thing.create() if (t.status.alive != false) return false if (t.status.name != "") return false if (t.status.score != 0.0) return false t.status.alive = true t.status.name = "orb" t.status.score = 3.5 if (!t.status.alive || t.status.name != "orb" || t.status.score != 3.5) return false return eve.EntityContainer == eve.Entity } function testStaticComponentsTable() { class Pos extends eve.Component { x = 1.0 } class Node extends eve.Entity { static components = { pos = Pos } pos = null } local n = Node.create() if (n.pos == null) return false if (n.pos.x != 1.0) return false return true } function testShaderSystemClassExists() { if (!("ShaderSystem" in eve)) return false // Construction without GPU is allowed; update() no-ops until setGpgpu. class Pos extends eve.Component { x = 0.0; y = 0.0 } class Vel extends eve.Component { x = 1.0; y = 0.0 } class Mover extends eve.Entity { pos = Pos; vel = Vel } local sys = eve.ShaderSystem(Mover) sys.bindFields(0, "pos", ["x", "y"]) sys.bindFields(1, "vel", ["x", "y"]) sys.update(0.016) return true } function testShaderSystemGpuResidentTransfers() { class Pos extends eve.Component { x = 0.0 } class Node extends eve.Entity { pos = Pos } class FakeBackend { dispatches = 0 records = 0 function ensureBuffer(binding, count) { return binding } function getBuffer(binding) { return binding } function dispatch(count, dt) { dispatches += 1 } function recordDispatch(sequence, count, dt) { records += 1 } } local uploads = 0 local downloads = 0 local uploadRange = null local downloadRange = null eve.packEcsFloats <- function(ents, slot, fields, buf) { uploads += 1 } eve.unpackEcsFloats <- function(ents, slot, fields, buf, count) { downloads += 1 } eve.packEcsFloatsRange <- function(ents, slot, fields, buf, first, count) { uploadRange = [first, count] } eve.unpackEcsFloatsRange <- function(ents, slot, fields, buf, first, count) { downloadRange = [first, count] } local node = Node.create() local second = Node.create() local third = Node.create() local sys = eve.ShaderSystem(Node) sys._gpu = true sys._backend = FakeBackend() sys.bindFields(0, "pos", ["x"], false, false) sys.update(0.016) sys.update(0.016) if (uploads != 1 || downloads != 0 || sys._backend.dispatches != 2) return false sys.requestUpload(0).requestReadback(0) sys.update(0.016) if (uploads != 2 || downloads != 1) return false sys.requestUploadRange(0, 1, 1).requestUploadRange(0, 2, 1) sys.requestReadbackRange(0, 1, 2) sys.update(0.016) if (uploadRange == null || uploadRange[0] != 1 || uploadRange[1] != 2) return false if (downloadRange == null || downloadRange[0] != 1 || downloadRange[1] != 2) return false if (uploads != 2 || downloads != 1) return false local batch = sys.record(true, 0.016) sys.completeRecorded(batch) if (sys._backend.records != 1) return false // Typed write-only schemas allocate resident output without packing stale CPU data. local outputSys = eve.ShaderSystem(Node) outputSys._gpu = true outputSys._backend = FakeBackend() outputSys.bindSchema(0, { slot = "pos", fields = ["x"], scalar = "f32", access = "write" }) outputSys.update(0.016) if (uploads != 2) return false local schema = outputSys.getBindingSchema(0) if (schema == null || schema.strideFloats != 1 || schema.access != "write") return false if (outputSys.shaderDeclarations("glsl") != "layout(set = 0, binding = 0) writeonly buffer EcsBinding0 { float data[]; } ecs0;\n") return false if (outputSys.shaderDeclarations("wgsl") != "@group(0) @binding(0) var ecs0 : array;\n") return false local rejected = false try { outputSys.bindSchema(1, { slot = "pos", fields = ["x"], scalar = "i32" }) } catch (e) { rejected = true } if (!rejected) return false // A structural revision invalidates resident packing even at the same capacity. local fourth = Node.create() sys.update(0.016) if (uploads != 3 || downloads != 1) return false node.destroy() second.destroy() third.destroy() fourth.destroy() return true } function testViewCacheStableAndInvalidated() { class Pos extends eve.Component { x = 0.0 } class Node extends eve.Entity { pos = Pos } class Sub extends Node {} local a = Node.create() local b = Sub.create() // No changes between calls → same cached array identity (zero allocation // per frame in System.entities()). local v1 = eve.view(Node) if (v1.len() != 2) return false local v2 = eve.view(Node) if (v1 != v2) return false local sv1 = eve.view(Sub) if (sv1.len() != 1) return false // create() invalidates the cached view (and subclass views via the chain) local c = Node.create() if (eve.view(Node).len() != 3) return false if (eve.view(Sub).len() != 1) return false // destroy() invalidates too; dead entities leave the view immediately a.destroy() b.destroy() c.destroy() if (eve.view(Node).len() != 0) return false return true } function testViewCacheBaseClassIncludesSubclasses() { class Pos extends eve.Component { x = 0.0 } class Node extends eve.Entity { pos = Pos } class Sub extends Node {} local n = Node.create() local s = Sub.create() if (eve.view(Node).len() != 2) return false if (eve.view(eve.Entity).len() < 2) return false n.destroy() s.destroy() return true } function testComponentDefaultsAndSlotsCached() { class C extends eve.Component { v = eve.Number; s = eve.String } class E extends eve.Entity { c = C } local a = E.create() if (a.c.v != 0.0 || a.c.s != "") return false a.c.v = 5.0 // Second instance gets fresh resolved defaults, not the mutated values. local b = E.create() if (b.c.v != 0.0 || b.c.s != "") return false if (a.c.v != 5.0) return false if (a.getComponent(C) != a.c) return false if (!a.hasComponent(C)) return false a.destroy() b.destroy() return true } function testGpuDrivenBindingSurface() { local gpu try { gpu = eve.Gpgpu() } catch (e) { return false } local required = ["setGpuDrivenEnabled", "isGpuDrivenEnabled", "gpuDrivenMeshRecord", "gpuDrivenMaterialRecord", "gpuDrivenMaterialUsable", "getGpuDrivenInstanceStride", "getGpuResidentOffsetAlignment", "writeGpuDrivenInstance", "submitResidentInstances"] foreach (name in required) if (!(name in gpu)) return false return gpu.getGpuDrivenInstanceStride() == 80 && gpu.getGpuResidentOffsetAlignment() == 256 } )SQ"; UnitSciptTest(ScriptEcsTest, kScriptEcsContent); TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.ready") { CHECK(vm.callFunc(vm.findFunc("testEcsReady"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.defineClassAndComponent") { CHECK(vm.callFunc(vm.findFunc("testDefineClassAndComponent"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewIncludesSubclass") { CHECK(vm.callFunc(vm.findFunc("testViewIncludesSubclass"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemUpdate") { CHECK(vm.callFunc(vm.findFunc("testSystemUpdate"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemDynamicSpawnDespawn") { CHECK(vm.callFunc(vm.findFunc("testSystemDynamicSpawnDespawn"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.systemQueryArrayAndSubclass") { CHECK(vm.callFunc(vm.findFunc("testSystemQueryArrayAndSubclass"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.typeMarkersAndEntityContainer") { CHECK(vm.callFunc(vm.findFunc("testTypeMarkersAndEntityContainer"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.staticComponentsTable") { CHECK(vm.callFunc(vm.findFunc("testStaticComponentsTable"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemClassExists") { CHECK(vm.callFunc(vm.findFunc("testShaderSystemClassExists"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.shaderSystemGpuResidentTransfers") { CHECK(vm.callFunc(vm.findFunc("testShaderSystemGpuResidentTransfers"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheStableAndInvalidated") { CHECK(vm.callFunc(vm.findFunc("testViewCacheStableAndInvalidated"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.viewCacheBaseClassIncludesSubclasses") { CHECK(vm.callFunc(vm.findFunc("testViewCacheBaseClassIncludesSubclasses"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.componentDefaultsAndSlotsCached") { CHECK(vm.callFunc(vm.findFunc("testComponentDefaultsAndSlotsCached"), vm).toBool()); } TEST_CASE_FIXTURE(ScriptEcsTest, "ScriptECS.gpuDrivenBindingSurface") { CHECK(vm.callFunc(vm.findFunc("testGpuDrivenBindingSurface"), vm).toBool()); }