Files
Calic 73298a99aa fix(test): use composition.reg_id for entity equality in assertions
get_children() returns fresh userdata wrappers on each call; without
an __eq metamethod, Lua pointer equality fails between two wrappers of
the same engine entity. Use composition.reg_id (a stable property on
all composition entities) for same_entity() comparisons in assertions
2b, 3b, 9, 10, 11b.
2026-06-13 17:41:59 +02:00

311 lines
12 KiB
Lua

-- lib-core.inventory-list-test — v0.1.0
-- 12 assertions: 1 engine spike + 11 spec assertions (B.2 test-spec).
-- Pattern follows P.2.4 Test-Module-Pattern; TAP output via engine.test.*
local composition = require("lib-core.composition")
local inventory = require("lib-core.inventory-list")
local T = engine.test
local M = {}
-- entity.get_children() returns fresh userdata wrappers on each call;
-- there is no __eq on entity userdata, so == is Lua-pointer equality
-- (always false for different wrappers of the same engine entity).
-- Compare by composition.reg_id (stable property on all composition
-- entities) instead.
local function same_entity(a, b)
local ra = a and a:get_property("composition.reg_id")
local rb = b and b:get_property("composition.reg_id")
if ra and ra ~= 0 and rb and rb ~= 0 then
return ra == rb
end
-- Fallback for raw entities: pointer equality (may be unreliable).
return a == b
end
-- ----------------------------------------------------------------
-- Template definitions (shared across tests)
-- All template names are unique within this test run.
-- ----------------------------------------------------------------
-- Container template: kind="list", no constraints.
composition.define_template{
id = "inv_test.container",
properties = {
sprite_path = "sprites/bag.png",
position = {x = 0, y = 0},
},
tags = {"renderable"},
container = { kind = "list" },
}
-- Item template: stack_mode="individual", has "item" tag + "renderable" tag.
composition.define_template{
id = "inv_test.sword",
properties = {
name = "Iron Sword",
stack_mode = "individual",
sprite_path = "sprites/sword.png",
position = {x = 0, y = 0},
},
tags = {"renderable", "item"},
}
-- A second item template for multi-item tests.
composition.define_template{
id = "inv_test.potion",
properties = {
name = "Health Potion",
stack_mode = "individual",
sprite_path = "sprites/potion.png",
position = {x = 0, y = 0},
},
tags = {"renderable", "item"},
}
-- Non-item template (no stack_mode property).
composition.define_template{
id = "inv_test.sign",
properties = {
text = "a sign",
sprite_path = "sprites/sign.png",
position = {x = 0, y = 0},
},
tags = {"renderable"},
}
-- Stack-item template (stack_mode="stack" — not supported in v0.1).
composition.define_template{
id = "inv_test.gold_coin",
properties = {
name = "Gold Coin",
stack_mode = "stack",
sprite_path = "sprites/coin.png",
position = {x = 0, y = 0},
},
tags = {"renderable", "item"},
}
-- Non-container template (no container block).
composition.define_template{
id = "inv_test.plain_entity",
properties = {
name = "plain",
},
}
function M.run_tests(ctx)
-- ================================================================
-- Spike: engine.entity supports multiple children on distinct slots
-- (locks the invariant that synthetic item.<n> naming is safe)
-- ================================================================
local spike_parent = entity.create()
local spike_a = entity.create()
local spike_b = entity.create()
local spike_c = entity.create()
entity.attach(spike_parent, "a", spike_a)
entity.attach(spike_parent, "b", spike_b)
entity.attach(spike_parent, "c", spike_c)
local spike_children = spike_parent:get_children()
local spike_n = 0
for _ in pairs(spike_children) do spike_n = spike_n + 1 end
T.assert(spike_n == 3 and
spike_children.a ~= nil and
spike_children.b ~= nil and
spike_children.c ~= nil,
"spike: engine.entity supports 3 children on distinct slot names")
-- ================================================================
-- Assertion 1: add(container, item) → contains returns true
-- ================================================================
local bag1 = composition.create{ template = "inv_test.container" }
local sword1 = composition.create{ template = "inv_test.sword" }
inventory.add(bag1, sword1)
T.assert(inventory.contains(bag1, sword1),
"1. add(container, item) → contains(container, item) is true")
-- ================================================================
-- Assertion 2: after add, count == 1 and contents contains item
-- ================================================================
T.assert(inventory.count(bag1) == 1,
"2a. after add: count(container) == 1")
local contents2 = inventory.contents(bag1)
local found_sword1 = false
for _, it in ipairs(contents2) do
if same_entity(it, sword1) then found_sword1 = true; break end
end
T.assert(found_sword1,
"2b. after add: contents(container) contains item")
-- ================================================================
-- Assertion 3: two adds of different items → count == 2,
-- both in contents, different engine slot names
-- ================================================================
local bag3 = composition.create{ template = "inv_test.container" }
local sword3 = composition.create{ template = "inv_test.sword" }
local potion3 = composition.create{ template = "inv_test.potion" }
inventory.add(bag3, sword3)
inventory.add(bag3, potion3)
T.assert(inventory.count(bag3) == 2,
"3a. two adds → count == 2")
local contents3 = inventory.contents(bag3)
local found3_s = false
local found3_p = false
for _, it in ipairs(contents3) do
if same_entity(it, sword3) then found3_s = true end
if same_entity(it, potion3) then found3_p = true end
end
T.assert(found3_s and found3_p,
"3b. two adds → both items in contents")
-- Verify no slot collision (different slot names).
local children3 = bag3:get_children()
local slots3 = {}
for slot, _ in pairs(children3) do table.insert(slots3, slot) end
T.assert(#slots3 == 2 and slots3[1] ~= slots3[2],
"3c. two adds → different engine slot names (no collision)")
-- ================================================================
-- Assertion 4: remove → contains false, count 0
-- ================================================================
local bag4 = composition.create{ template = "inv_test.container" }
local sword4 = composition.create{ template = "inv_test.sword" }
inventory.add(bag4, sword4)
inventory.remove(bag4, sword4)
T.assert(not inventory.contains(bag4, sword4),
"4a. remove → contains(container, item) is false")
T.assert(inventory.count(bag4) == 0,
"4b. remove → count == 0")
-- ================================================================
-- Assertion 5: add to entity without container-block → Loud-Error
-- ================================================================
local plain5 = composition.create{ template = "inv_test.plain_entity" }
local sword5 = composition.create{ template = "inv_test.sword" }
local ok5 = pcall(inventory.add, plain5, sword5)
T.assert(not ok5,
"5. add to entity without container block → Loud-Error")
-- ================================================================
-- Assertion 6: add of entity without stack_mode → Loud-Error
-- ================================================================
local bag6 = composition.create{ template = "inv_test.container" }
local sign6 = composition.create{ template = "inv_test.sign" }
local ok6 = pcall(inventory.add, bag6, sign6)
T.assert(not ok6,
"6. add entity without stack_mode (not an item) → Loud-Error")
-- ================================================================
-- Assertion 7: add item with stack_mode="stack" → Loud-Error
-- ================================================================
local bag7 = composition.create{ template = "inv_test.container" }
local coin7 = composition.create{ template = "inv_test.gold_coin" }
local ok7 = pcall(inventory.add, bag7, coin7)
T.assert(not ok7,
"7. add item with stack_mode='stack' → Loud-Error")
-- ================================================================
-- Assertion 8: add on non-composition entity (no reg_id) → Loud-Error
-- Uses raw entity.create() (not composition.create) so the entity
-- has no composition.reg_id. composition.get_container returns nil,
-- which propagates through validate_container as a Loud-Error.
-- Distinct from assertion 5 (composition entity, no container block).
-- ================================================================
local raw8 = entity.create()
local sword8 = composition.create{ template = "inv_test.sword" }
local ok8 = pcall(inventory.add, raw8, sword8)
T.assert(not ok8,
"8. add on non-composition entity (no reg_id) → Loud-Error")
-- ================================================================
-- Assertion 9: add sets renderable tag off on item
-- ================================================================
local bag9 = composition.create{ template = "inv_test.container" }
local sword9 = composition.create{ template = "inv_test.sword" }
-- Confirm sword is renderable before add.
local renderable_before9 = composition.list_by_tag("renderable")
local in_renderable_before9 = false
for _, e in ipairs(renderable_before9) do
if same_entity(e, sword9) then in_renderable_before9 = true; break end
end
T.assert(in_renderable_before9,
"9a. item is in renderable index before add")
inventory.add(bag9, sword9)
local renderable_after9 = composition.list_by_tag("renderable")
local in_renderable_after9 = false
for _, e in ipairs(renderable_after9) do
if same_entity(e, sword9) then in_renderable_after9 = true; break end
end
T.assert(not in_renderable_after9,
"9b. add: item removed from renderable index via composition.set_tag")
-- ================================================================
-- Assertion 10: remove sets renderable tag back on item
-- ================================================================
inventory.remove(bag9, sword9)
local renderable_after_remove10 = composition.list_by_tag("renderable")
local restored10 = false
for _, e in ipairs(renderable_after_remove10) do
if same_entity(e, sword9) then restored10 = true; break end
end
T.assert(restored10,
"10. remove: item restored to renderable index via composition.set_tag")
-- ================================================================
-- Assertion 11: slot-naming reconstruction
-- A container already has 2 children from a prior session (simulated
-- by directly attaching raw entities under item.3 and item.7 slots),
-- then inventory.contents and inventory.count derive correct results
-- from get_children, and next add lands on item.8 (max+1).
-- ================================================================
local bag11 = composition.create{ template = "inv_test.container" }
-- Simulate pre-existing items (as if loaded from persistence).
-- We create composition items so set_tag works, then directly attach
-- them under specific slot names to mimic a reloaded state.
local item11a = composition.create{ template = "inv_test.sword" }
local item11b = composition.create{ template = "inv_test.potion" }
-- Attach directly at known slot indices (simulating a reload state).
entity.attach(bag11, "item.3", item11a)
entity.attach(bag11, "item.7", item11b)
-- contents/count must work purely from get_children.
T.assert(inventory.count(bag11) == 2,
"11a. reconstruction: count derives 2 from existing item.<n> children")
local contents11 = inventory.contents(bag11)
T.assert(#contents11 == 2 and
same_entity(contents11[1], item11a) and
same_entity(contents11[2], item11b),
"11b. reconstruction: contents ordered by slot-n, items correct")
-- Next add must land on item.8 (max=7, so 7+1=8), not item.1.
local item11c = composition.create{ template = "inv_test.sword" }
-- item11c has no prior parent; set_tag is invoked by add.
inventory.add(bag11, item11c)
local children11 = bag11:get_children()
T.assert(children11["item.8"] ~= nil,
"11c. reconstruction: next synthetic slot after item.7 is item.8")
end
return M