Files
plainleaf/client/space_lua/metamethods_test.lua
T

572 lines
16 KiB
Lua

local function assertEquals(actual, expected, message)
if actual ~= expected then
error('Assertion failed: ' .. message
.. ' (expected ' .. tostring(expected)
.. ', got ' .. tostring(actual) .. ')')
end
end
local function assertTrue(v, message)
if v ~= true then
error('Assertion failed: ' .. message .. ' (expected true)')
end
end
local function assertFalse(v, message)
if v ~= false then
error('Assertion failed: ' .. message .. ' (expected false)')
end
end
local function assertThrows(msg_substr, fn)
local ok, err = pcall(fn)
if ok then
error('Assertion failed: expected error containing "'
.. msg_substr .. '"')
end
if type(err) ~= 'string' then
err = tostring(err)
end
if not string.find(err, msg_substr, 1, true) then
error('Assertion failed: expected error message to contain "'
.. msg_substr .. '", got: "' .. err .. '"')
end
end
-- 1. Arithmetic + bitwise metamethods: values + multi-return trimming
do
local mt = {}
mt.__add = function(a, b) return a.tag .. '+' .. b.tag end
mt.__sub = function(a, b) return a.tag .. '-' .. b.tag end
mt.__mul = function(a, b) return a.tag .. '*' .. b.tag end
mt.__div = function(a, b) return a.tag .. '/' .. b.tag end
mt.__pow = function(a, b) return a.tag .. '^' .. b.tag end
mt.__idiv = function(a, b) return a.tag .. '//' .. b.tag end
mt.__mod = function(a, b) return a.tag .. '%' .. b.tag end
mt.__unm = function(a) return '(-' .. a.tag .. ')' end
mt.__bnot = function(a) return '(~' .. a.tag .. ')' end
mt.__band = function(a, b) return a.tag .. '&' .. b.tag end
mt.__bor = function(a, b) return a.tag .. '|' .. b.tag end
mt.__bxor = function(a, b) return a.tag .. '~' .. b.tag end
mt.__shl = function(a, b) return a.tag .. '<<' .. b.tag end
mt.__shr = function(a, b) return a.tag .. '>>' .. b.tag end
local a = setmetatable({ tag = 'A' }, mt)
local b = setmetatable({ tag = 'B' }, mt)
assertEquals(a + b, 'A+B', '__add produced correct value')
assertEquals(a - b, 'A-B', '__sub produced correct value')
assertEquals(a * b, 'A*B', '__mul produced correct value')
assertEquals(a / b, 'A/B', '__div produced correct value')
assertEquals(a ^ b, 'A^B', '__pow produced correct value')
assertEquals(a // b, 'A//B', '__idiv produced correct value')
assertEquals(a % b, 'A%B', '__mod produced correct value')
assertEquals(-a, '(-A)', '__unm produced correct value')
assertEquals(~a, '(~A)', '__bnot produced correct value')
assertEquals(a & b, 'A&B', '__band produced correct value')
assertEquals(a | b, 'A|B', '__bor produced correct value')
assertEquals(a ~ b, 'A~B', '__bxor produced correct value')
assertEquals(a << b, 'A<<B', '__shl produced correct value')
assertEquals(a >> b, 'A>>B', '__shr produced correct value')
-- Multi-return from metamethod: must take only the first return
do
local mt_mr = {
__add = function(_, _) return 'first', 'second' end,
__unm = function(_) return 7, 8 end,
__bnot = function(_) return 9, 10 end,
__concat = function(_, _) return "c1", "c2" end,
__len = function(_) return 123, 999 end,
}
local x = setmetatable({}, mt_mr)
assertEquals(x + x, 'first', 'binary operator uses first return value')
assertEquals(-x, 7, 'unary operator uses first return value')
assertEquals(~x, 9, 'unary operator uses first return value')
assertEquals(x .. x, "c1", 'concat uses first return value')
assertEquals(#x, 123, 'len uses first return value')
end
end
-- 2. Comparison operators
do
-- 2.1. No ordering for plain tables
assertThrows("attempt to compare", function() return ({} < {}) end)
assertThrows("attempt to compare", function() return ({} <= {}) end)
assertThrows("attempt to compare", function() return ({} > {}) end)
assertThrows("attempt to compare", function() return ({} >= {}) end)
-- 2.2. With metamethods: `__lt` / `__le`
do
local mt = {
__lt = function(a, b) return a.v < b.v end,
__le = function(a, b) return a.v <= b.v end,
}
local function O(v) return setmetatable({ v = v }, mt) end
assertTrue(O(1) < O(2), "__lt true")
assertFalse(O(2) < O(1), "__lt false")
assertTrue(O(2) <= O(2), "__le true")
assertFalse(O(3) <= O(2), "__le false")
end
-- 2.3. `__eq` semantics
do
do
local a, b = {}, {}
assertFalse(a == b, "raw equality for distinct tables without __eq is false")
assertTrue(a == a, "raw equality for same table is true")
end
do
local calls = 0
local mt = {
__eq = function(_, _)
calls = calls + 1
return true
end
}
local x = setmetatable({}, mt)
local y = {} -- no metatable
assertTrue(x == y, "__eq may be used from left operand metatable")
assertTrue(y == x, "__eq may be used from right operand metatable")
assertTrue(calls >= 1, "__eq was called at least once")
end
do
local a = setmetatable({ v = 1 }, { __eq = function() return true end })
local b = setmetatable({ v = 2 }, { __eq = function() return true end })
assertTrue(a == b, "__eq true/true => true")
assertFalse(a ~= b, "~= is negation of ==")
end
do
local a = setmetatable({ v = 1 }, { __eq = function() return false end })
local b = setmetatable({ v = 2 }, { __eq = function() return false end })
assertFalse(a == b, "__eq false/false => false")
assertTrue(a ~= b, "~= is negation of ==")
end
end
-- 2.4. Operand swap rules: `a > b` is `b < a` and `a >= b` is `b <= a`
do
local mt2 = { __lt = function(a, b) return a.v < b.v end }
local function P(v) return setmetatable({ v = v }, mt2) end
local a = P(3)
local b = P(2)
assertTrue(b < a, "sanity: b < a uses __lt")
assertTrue(a > b, '">" uses swapped __lt')
assertFalse(b > a, '">" uses swapped __lt (false case)')
end
do
local mt3 = { __le = function(a, b) return a.v <= b.v end }
local function P(v) return setmetatable({ v = v }, mt3) end
local a = P(3)
local b = P(2)
assertTrue(b <= a, "sanity: b <= a uses __le")
assertTrue(a >= b, '">=" uses swapped __le')
assertFalse(b >= a, '">=" uses swapped __le (false case)')
end
-- 2.5. No `__le` fallback via `__lt` in Lua for metamethod comparisons
do
local mt = { __lt = function(a, b) return a.v < b.v end }
local function P(v) return setmetatable({ v = v }, mt) end
local a = P(2)
local b = P(2)
assertFalse(a < b, "sanity: 2 < 2 is false via __lt")
assertThrows("attempt to compare", function() return a <= b end)
end
do
local mt = {
__lt = function(a, b) return a.v < b.v end,
__le = function(a, b) return a.v <= b.v end,
}
local function P(v) return setmetatable({ v = v }, mt) end
local a = P(2)
local b = P(2)
assertTrue(a <= b, "__le works when provided")
end
end
-- 3. `rawlen` vs `#` when `__len` exists
do
local t = setmetatable({ 1, 2, 3 }, { __len = function() return 10 end })
assertEquals(#t, 10, '# uses __len')
assertEquals(rawlen(t), 3, 'rawlen ignores __len on tables')
assertEquals(rawlen("abc"), 3, 'rawlen works on strings')
end
-- 4. `__len`: tables use metamethod; strings are raw length
do
local t = setmetatable({ x = 20 }, { __len = function(tt) return tt.x end })
assertEquals(#t, 20, '__len dispatched for tables')
t.x = "234"
assertEquals(#t, "234", '__len can return non-number')
end
do
local s = "abc"
assertEquals(#s, 3, 'strings ignore __len metamethod')
end
-- 5. `__concat`: produced value and number/table interop
do
local a = setmetatable({ x = "u" }, {
__concat = function(l, r) return l.x .. "." .. r.x end
})
assertEquals(a .. a, "u.u", '__concat dispatched')
end
do
local c = {}
setmetatable(c, {
__concat = function(a, b)
if type(a) == "number" then
assertEquals(b, c, '__concat rhs is c when lhs number')
return "n..c"
else
assertEquals(a, c, '__concat lhs is c when rhs number')
return "c..n"
end
end
})
assertEquals(c .. 5, "c..n", "__concat handles table .. number")
assertEquals(5 .. c, "n..c", "__concat handles number .. table")
end
-- 6. `__call`: produced values
do
local t = setmetatable({}, {
__call = function(self, a, b)
assertEquals(type(self), 'table', '__call self is table')
return a + b
end
})
assertEquals(t(2, 3), 5, '__call dispatched')
end
do
local t = setmetatable({}, {
__call = function(_, ...)
return "a", "b", select('#', ...)
end
})
local a, b, n = t(10, 20)
assertEquals(a, "a", "__call multi-return first")
assertEquals(b, "b", "__call multi-return second")
assertEquals(n, 2, "__call sees arguments")
end
do
local n = 200
local function leaf()
if n == 0 then return 1023 end
n = n - 1
return leaf()
end
local f = leaf
for _ = 1, 50 do
f = setmetatable({}, { __call = f })
end
assertEquals(f(), 1023, 'chain of __call metamethods works')
end
do
local i = 0
local tt = {
__call = function(t, ...)
i = i + 1
if t.f then return t.f(...) end
return {...}
end
}
local a = setmetatable({}, tt)
local b = setmetatable({ f = a }, tt)
local c = setmetatable({ f = b }, tt)
local x = c(3, 4, 5)
assertEquals(i, 3, "__call nested chain increments i")
assertEquals(x[1], 3, "__call nested chain arg1")
assertEquals(x[2], 4, "__call nested chain arg2")
assertEquals(x[3], 5, "__call nested chain arg3")
end
do
local t = setmetatable({}, { __call = 123 })
assertThrows("attempt to call a number value", function()
return t()
end)
end
-- 7. `__index` / `__newindex`: function + table forms
do
local backing = { x = 10 }
local t = setmetatable({}, {
__index = function(_, k) return backing[k] end,
__newindex = function(_, k, v) backing[k] = v end,
})
assertEquals(t.x, 10, '__index function dispatched')
t.y = 20
assertEquals(backing.y, 20, '__newindex function dispatched')
end
do
local backing = { a = 1, b = 2 }
local t = setmetatable({}, { __index = backing })
assertEquals(t.a, 1, '__index table dispatched (a)')
assertEquals(t.b, 2, '__index table dispatched (b)')
assertEquals(t.c, nil, '__index table missing key yields nil')
end
do
-- __newindex as table target (Lua 5.4)
local backing = {}
local t = setmetatable({}, { __newindex = backing })
t.k = 99
assertEquals(backing.k, 99, '__newindex table dispatched')
end
do
local calls = 0
local t = { present = 1 }
setmetatable(t, {
__newindex = function(tt, k, v)
calls = calls + 1
rawset(tt, k, v)
end
})
t.present = 2
assertEquals(calls, 0, '__newindex not called for existing key')
t.absent = 3
assertEquals(calls, 1, '__newindex called for absent key')
assertEquals(t.absent, 3, '__newindex stored value')
end
do
local a = {}
setmetatable(a, a)
a.__index = a
a.__newindex = a
assertThrows("chain too long", function()
local _ = a[10]
end)
assertThrows("chain too long", function()
a[10] = true
end)
end
-- 8. `__pairs` and `ipairs` via `__index`-backed table
do
local a = {}
local out = {}
local function foo(e, i)
assertEquals(e, a, "__pairs sees original table")
if i < 10 then
return i + 1, i + 2
end
end
setmetatable(a, { __pairs = function(x) return foo, x, 0 end })
local i = 0
for k, v in pairs(a) do
i = i + 1
out[i] = { k, v }
end
assertEquals(i, 10, "__pairs loop count")
assertEquals(out[1][1], 1, "__pairs k1")
assertEquals(out[1][2], 2, "__pairs v1")
assertEquals(out[10][1], 10, "__pairs k10")
assertEquals(out[10][2], 11, "__pairs v10")
end
do
local a = { n = 5 }
setmetatable(a, {
__index = function(t, k)
if type(k) == "number" and k <= t.n then
return k * 10
end
end
})
local out = {}
for k, v in ipairs(a) do
out[#out + 1] = k .. ":" .. v
end
assertEquals(#out, 5, "ipairs iterates n entries via __index")
assertEquals(out[1], "1:10", "ipairs entry 1")
assertEquals(out[5], "5:50", "ipairs entry 5")
end
-- 9. `table` library interaction with metamethods
do
local function check_proxy(proxy, backing)
for i = 1, 10 do
table.insert(proxy, 1, i)
end
assertEquals(#proxy, 10, "proxy length after inserts")
assertEquals(#backing, 10, "backing length after inserts")
for i = 1, 10 do
assertEquals(backing[i], 11 - i, "backing reversed insert " .. i)
end
table.sort(proxy)
for i = 1, 10 do
assertEquals(backing[i], i, "backing sorted " .. i)
assertEquals(proxy[i], i, "proxy sorted " .. i)
end
assertEquals(table.concat(proxy, ","), "1,2,3,4,5,6,7,8,9,10", "table.concat(proxy)")
for i = 1, 8 do
assertEquals(table.remove(proxy, 1), i, "table.remove(proxy,1) yields i")
end
assertEquals(#proxy, 2, "proxy length after removes")
assertEquals(#backing, 2, "backing length after removes")
local a, b, c = table.unpack(proxy)
assertEquals(a, 9, "table.unpack(proxy) first")
assertEquals(b, 10, "table.unpack(proxy) second")
assertEquals(c, nil, "table.unpack(proxy) third nil")
end
local backing = {}
local proxy = setmetatable({}, {
__len = function() return #backing end,
__index = backing,
__newindex = backing,
})
check_proxy(proxy, backing)
end
-- 10. Comparison compatibility corner: metamethod dispatch present
do
local mt1 = {
__eq = function() return true end,
__lt = function() return true end,
__le = function() return false end,
}
local c = setmetatable({}, mt1)
local d = setmetatable({}, mt1)
assertTrue(c == d, "comparison compat: __eq true (same mt)")
assertTrue(c < d, "comparison compat: __lt true (same mt)")
assertFalse(d <= c, "comparison compat: __le false (same mt)")
local mt2 = {
__eq = mt1.__eq,
__lt = mt1.__lt,
__le = mt1.__le,
}
local e = setmetatable({}, mt1)
local f = setmetatable({}, mt2)
assertTrue(e == f, "comparison compat across metatables: __eq")
assertTrue(e < f, "comparison compat across metatables: __lt")
assertFalse(f <= e, "comparison compat across metatables: __le")
end
-- 11. Concat chain associativity + returning table from `__concat`
do
local t = {}
t.__concat = function(a, b)
local av = (type(a) == "table") and a.val or tostring(a)
local bv = (type(b) == "table") and b.val or tostring(b)
return setmetatable({ val = av .. bv }, t)
end
local c = setmetatable({ val = "c" }, t)
local d = setmetatable({ val = "d" }, t)
local x = c .. d .. c .. d
assertEquals(type(x), "table", "__concat chain returns table")
assertEquals(x.val, "cdcd", "__concat chain value")
end
-- 12. `__eq` is not used for table indexing / hashing
do
local mt = {}
mt.__eq = function(_, _) return true end
local function Set(x)
local t = {}
for _, k in pairs(x) do t[k] = true end
return setmetatable(t, mt)
end
local k1 = Set{1, 2, 3}
local k2 = Set{1, 2, 3}
assertTrue(k1 == k2, "__eq says equal")
assertFalse(rawequal(k1, k2), "not the same object")
local t = {}
t[k1] = 123
assertEquals(t[k2], nil, "table indexing ignores __eq")
assertEquals(t[k1], 123, "table indexing by same object works")
end
-- 13. operator metamethod lookup must be raw (no `__index` involvement)
do
local mt = {
__index = function(_, _)
return function() return "BAD" end
end
}
local a = setmetatable({}, mt)
assertThrows("attempt to perform arithmetic on a table value", function()
local _ = a + a
end)
end
-- 14. `__tostring`: must return a string
do
local m = setmetatable({ name = "NAME" }, {
__tostring = function(x) return x.name end
})
assertEquals(tostring(m), "NAME", "__tostring used by tostring()")
end
do
local m = setmetatable({}, {
__tostring = function() return {} end
})
assertThrows("'__tostring' must return a string", function()
return tostring(m)
end)
end